From 5f716f50088d0b832b099ac15a804521c0a8b914 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 16 Oct 2021 11:30:51 +0200 Subject: [PATCH 01/33] Use MultiTypeAdapter as a backend for RecyclerView stuff --- .../moire/ultrasonic/domain/Identifiable.kt | 5 + dependencies.gradle | 2 + ultrasonic/build.gradle | 2 + .../moire/ultrasonic/util/LoadingTask.java | 1 + .../{fragment => adapters}/AlbumRowAdapter.kt | 2 +- .../ArtistRowAdapter.kt | 2 +- .../GenericRowAdapter.kt | 2 +- .../moire/ultrasonic/adapters/ImageHelper.kt | 45 ++ .../adapters/MultiTypeDiffAdapter.kt | 157 ++++ .../ServerRowAdapter.kt | 1 + .../ultrasonic/adapters/TrackViewBinder.kt | 94 +++ .../ultrasonic/adapters/TrackViewHolder.kt | 306 ++++++++ .../filepicker/FilePickerAdapter.kt | 8 +- .../ultrasonic/fragment/AlbumListFragment.kt | 1 + .../ultrasonic/fragment/ArtistListFragment.kt | 1 + .../ultrasonic/fragment/DownloadsFragment.kt | 102 +-- .../fragment/GenericListFragment.kt | 1 + .../ultrasonic/fragment/MultiListFragment.kt | 284 ++++++++ .../ultrasonic/fragment/PlayerFragment.kt | 8 +- .../fragment/ServerSelectorFragment.kt | 1 + .../fragment/TrackCollectionFragment.kt | 668 ++++++++++-------- .../fragment/TrackCollectionModel.kt | 25 +- .../moire/ultrasonic/service/DownloadFile.kt | 3 + .../moire/ultrasonic/service/Downloader.kt | 6 + .../kotlin/org/moire/ultrasonic/util/Util.kt | 85 ++- .../moire/ultrasonic/view/SongViewHolder.kt | 316 +++++++++ ultrasonic/src/main/res/layout/track_list.xml | 48 ++ .../main/res/navigation/navigation_graph.xml | 7 + 28 files changed, 1746 insertions(+), 437 deletions(-) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => adapters}/AlbumRowAdapter.kt (99%) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => adapters}/ArtistRowAdapter.kt (98%) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => adapters}/GenericRowAdapter.kt (99%) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => adapters}/ServerRowAdapter.kt (99%) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt create mode 100644 ultrasonic/src/main/res/layout/track_list.xml diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Identifiable.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Identifiable.kt index b361d0b7..b316ce8e 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Identifiable.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Identifiable.kt @@ -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 { val id: String + val longId: Long } diff --git a/dependencies.gradle b/dependencies.gradle index 24bb2e52..33ad5dd2 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -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 = [ diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 27d06bc3..934e3b0d 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -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 diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LoadingTask.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LoadingTask.java index f42771c0..16936c09 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LoadingTask.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LoadingTask.java @@ -19,6 +19,7 @@ public abstract class LoadingTask extends BackgroundTask this.cancel = cancel; } + @Override public void execute() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowAdapter.kt similarity index 99% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowAdapter.kt index e203a35e..61c79fea 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowAdapter.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowAdapter.kt similarity index 98% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowAdapter.kt index d4079a73..88837b68 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowAdapter.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/GenericRowAdapter.kt similarity index 99% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/GenericRowAdapter.kt index 33f3783e..e95fb2c5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/GenericRowAdapter.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt new file mode 100644 index 00000000..174c449c --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt @@ -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) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt new file mode 100644 index 00000000..7bb0e251 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt @@ -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 : MultiTypeAdapter() { + + val diffCallback = GenericDiffCallback() + var tracker: SelectionTracker? = null + + init { + setHasStableIds(true) + } + + override fun getItemId(position: Int): Long { + return getItem(position).longId + } + + override var items: List + get() = getCurrentList() + set(value) { + throw Exception("You must use submitList() to add data to the MultiTypeDiffAdapter") + } + + + var mDiffer: AsyncListDiffer = AsyncListDiffer( + AdapterListUpdateCallback(this), + AsyncDifferConfig.Builder(diffCallback).build() + ) + + private val mListener = + ListListener { 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?) { + 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?, 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 { + 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, currentList: List) { + // Void + } + + + + + + companion object { + /** + * Calculates the differences between data sets + */ + class GenericDiffCallback : DiffUtil.ItemCallback() { + @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 + } + } + + + + + } + +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt similarity index 99% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerRowAdapter.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt index 2b9e4be1..de8c1590 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt @@ -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 /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt new file mode 100644 index 00000000..e5e74d2d --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -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, + val checkable: Boolean, + val draggable: Boolean, + context: Context +) : ItemViewBinder(), 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) +// } +// ) + } + + +} + + + diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt new file mode 100644 index 00000000..116a2852 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -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) : + 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 + } + + + +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/FilePickerAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/FilePickerAdapter.kt index 5c4dcbc7..a902e443 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/FilePickerAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/FilePickerAdapter.kt @@ -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() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index aafdea81..2070278c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 58ee16a8..989c4b7b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index b9ca729a..a3a69fe2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -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() { @@ -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( 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 } } + + + diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt index 1aa1f254..c5df2b4f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt new file mode 100644 index 00000000..e3604620 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -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 : 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> + + /** + * The central function to pass a query to the model and return a LiveData object + */ + abstract fun getLiveData(args: Bundle? = null): LiveData> + + /** + * 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 -> + //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(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> : +// GenericListFragment() { +// @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) +// } +//} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 7556d148..ef7da49b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -217,7 +217,7 @@ class PlayerFragment : val ratingLinearLayout = view.findViewById(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 -> { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt index 05d7b568..f06ed156 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 6394b08a..a2c379a7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -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>() { - 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 = 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(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 = 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 = 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) { - 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 { - val songs: MutableList = ArrayList(10) + private fun getSelectedSongs(): MutableList { + val songs: MutableList = 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 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> { + 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 + } + + } + + + diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt index ab5ce052..0c547c20 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt @@ -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 = MutableLiveData() + val currentList: MutableLiveData> = MutableLiveData() val songsForGenre: MutableLiveData = 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) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index 86b9aeab..4f0260eb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index a1aa4bec..51db86d1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -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) + } } + diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index baf983a4..d84fb080 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -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( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt new file mode 100644 index 00000000..020b9ac9 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt @@ -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 + } + + +} \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/track_list.xml b/ultrasonic/src/main/res/layout/track_list.xml new file mode 100644 index 00000000..76a702c4 --- /dev/null +++ b/ultrasonic/src/main/res/layout/track_list.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/ultrasonic/src/main/res/navigation/navigation_graph.xml b/ultrasonic/src/main/res/navigation/navigation_graph.xml index 4f4bd844..c76a0720 100644 --- a/ultrasonic/src/main/res/navigation/navigation_graph.xml +++ b/ultrasonic/src/main/res/navigation/navigation_graph.xml @@ -38,6 +38,13 @@ + + Date: Mon, 18 Oct 2021 12:57:21 +0200 Subject: [PATCH 02/33] Add a HeaderView binder --- .../moire/ultrasonic/util/AlbumHeader.java | 114 ---- .../org/moire/ultrasonic/util/AlbumHeader.kt | 97 +++ .../ultrasonic/adapters/HeaderViewBinder.kt | 105 +++ .../ultrasonic/adapters/TrackViewBinder.kt | 6 +- .../ultrasonic/fragment/DownloadsFragment.kt | 102 +-- .../ultrasonic/fragment/MultiListFragment.kt | 14 +- .../fragment/TrackCollectionFragment.kt | 166 ++--- .../kotlin/org/moire/ultrasonic/util/Util.kt | 2 +- .../moire/ultrasonic/view/SongViewHolder.kt | 614 +++++++++--------- 9 files changed, 595 insertions(+), 625 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.java create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.java deleted file mode 100644 index 947b86f1..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.java +++ /dev/null @@ -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 artists; - private Set grandParents; - private Set genres; - private Set years; - - public boolean getIsAllVideo() - { - return isAllVideo; - } - - public long getTotalDuration() - { - return totalDuration; - } - - public Set getArtists() - { - return artists; - } - - public Set getGrandParents() - { - return this.grandParents; - } - - public Set getGenres() - { - return this.genres; - } - - public Set getYears() - { - return this.years; - } - - public AlbumHeader() - { - this.artists = new HashSet(); - this.grandParents = new HashSet(); - this.genres = new HashSet(); - this.years = new HashSet(); - - this.isAllVideo = true; - this.totalDuration = 0; - } - - public static AlbumHeader processEntries(Context context, Iterable 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); - } - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt new file mode 100644 index 00000000..522e94ef --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt @@ -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, + var name: String, + songCount: Int +): Identifiable { + var isAllVideo: Boolean + private set + + var totalDuration: Long + private set + + var childCount = 0 + + private val _artists: MutableSet + private val _grandParents: MutableSet + private val _genres: MutableSet + private val _years: MutableSet + + val artists: Set + get() = _artists + + val grandParents: Set + get() = _grandParents + + val genres: Set + get() = _genres + + val years: Set + 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) { + 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) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt new file mode 100644 index 00000000..4b3f6fd0 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt @@ -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(), KoinComponent { + + private val weakContext: WeakReference = 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 + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index e5e74d2d..1533f67c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -1,11 +1,8 @@ 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 @@ -14,7 +11,6 @@ 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, @@ -70,7 +66,7 @@ class TrackViewBinder( checkable = checkable, draggable = draggable ) - + // Observe download status // item.status.observe( // lifecycleOwner, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index a3a69fe2..0af78788 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -1,44 +1,27 @@ package org.moire.ultrasonic.fragment import android.app.Application -import android.content.Context import android.os.Bundle import android.view.MenuItem -import android.view.View 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.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.Downloader import org.moire.ultrasonic.util.Util -import org.moire.ultrasonic.view.SongViewHolder +import java.util.TreeSet -class DownloadsFragment : GenericListFragment() { +class DownloadsFragment : MultiListFragment>() { /** * The ViewModel to use to get the data */ 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, * after the user has clicked on an item @@ -56,15 +39,17 @@ class DownloadsFragment : GenericListFragment( /** * Provide the Adapter for the RecyclerView with a lazy delegate */ - override val viewAdapter: DownloadRowAdapter by lazy { - DownloadRowAdapter( - liveDataItems.value ?: listOf(), - { entry -> onItemClick(entry) }, - { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, - onMusicFolderUpdate, - requireContext(), - viewLifecycleOwner + override val viewAdapter: MultiTypeDiffAdapter by lazy { + val adapter = MultiTypeDiffAdapter() + adapter.register( + TrackViewBinder( + selectedSet = TreeSet(), + checkable = false, + draggable = false, + context = requireContext() + ) ) + adapter } override fun onContextMenuItemSelected(menuItem: MenuItem, item: DownloadFile): Boolean { @@ -81,63 +66,6 @@ class DownloadsFragment : GenericListFragment( } } -class DownloadRowAdapter( - itemList: List, - onItemClick: (DownloadFile) -> Unit, - onContextMenuClick: (MenuItem, DownloadFile) -> Boolean, - onMusicFolderUpdate: (String?) -> Unit, - val context: Context, - val lifecycleOwner: LifecycleOwner -) : GenericRowAdapter( - 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) { private val downloader by inject() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index e3604620..18009be2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -71,20 +71,20 @@ abstract class MultiListFragment : Frag */ 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 + open val mainLayout: Int = R.layout.generic_list /** * 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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index a2c379a7..1cf42767 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -31,13 +31,13 @@ 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.HeaderViewBinder 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.NetworkAndStorageChecker @@ -51,17 +51,18 @@ import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util 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 + * TODO: Move Clickhandler into ViewBinders + * TODO: Migrate Album/artistsRow + * TODO: Wrong count (selectall) + * TODO: Handle updates (playstatus, download status) */ class TrackCollectionFragment : MultiListFragment>() { - private var header: View? = null private var albumButtons: View? = null private var emptyView: TextView? = null private var selectButton: ImageView? = null @@ -84,7 +85,6 @@ class TrackCollectionFragment : private var cancellationToken: CancellationToken? = null override val listModel: TrackCollectionModel by viewModels() - private val random: Random = Random() private var selectedSet: TreeSet = TreeSet() @@ -136,11 +136,6 @@ class TrackCollectionFragment : updateDisplay(true) } - header = LayoutInflater.from(context).inflate( - R.layout.select_album_header, listView, - false - ) - listModel.currentList.observe(viewLifecycleOwner, defaultObserver) listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver) @@ -231,17 +226,25 @@ class TrackCollectionFragment : } + viewAdapter.register( + HeaderViewBinder( + context = requireContext() + ) + ) + viewAdapter.register( TrackViewBinder( selectedSet = selectedSet, checkable = true, draggable = false, - context = context!! + context = requireContext() ) ) + enableButtons() + // Loads the data updateDisplay(false) } @@ -253,6 +256,7 @@ class TrackCollectionFragment : } private fun updateDisplay(refresh: Boolean) { + // FIXME: Use refresh getLiveData(requireArguments()) } @@ -383,12 +387,32 @@ class TrackCollectionFragment : } } + private val viewHolders: List + get() { + val list: MutableList = 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) { var hasSubFolders = false - for (i in 0 until listView!!.childCount) { - val vh = listView!!.findViewHolderForAdapterPosition(i) as TrackViewHolder? - val entry = vh?.entry + for (vh in viewHolders) { + val entry = vh.entry if (entry != null && entry.isDirectory) { hasSubFolders = true break @@ -427,20 +451,19 @@ class TrackCollectionFragment : } private fun selectAllOrNone() { - val someUnselected = selectedSet.size < listView!!.childCount + val someUnselected = selectedSet.size < childCount selectAll(someUnselected, true) } private fun selectAll(selected: Boolean, toast: Boolean) { - val count = listView!!.childCount + var selectedCount = 0 listView!! - for (i in 0 until count) { - val vh = listView!!.findViewHolderForAdapterPosition(i) as TrackViewHolder + for (vh in viewHolders) { val entry = vh.entry if (entry != null && !entry.isDirectory && !entry.isVideo) { @@ -579,16 +602,19 @@ class TrackCollectionFragment : private val defaultObserver = Observer(this::updateInterfaceWithEntries) - private fun updateInterfaceWithEntries(list: List) { + private fun updateInterfaceWithEntries(newList: List) { + + val entryList: MutableList = newList.toMutableList() if (listModel.currentListIsSortable && Settings.shouldSortByDisc) { - Collections.sort(list, EntryByDiscAndTrackComparator()) + Collections.sort(entryList, EntryByDiscAndTrackComparator()) } + var allVideos = true var songCount = 0 - for (entry in list) { + for (entry in entryList) { if (!entry.isVideo) { allVideos = false } @@ -600,18 +626,6 @@ class TrackCollectionFragment : val listSize = requireArguments().getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 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 unpinButton!!.visibility = View.VISIBLE downloadButton!!.visibility = View.VISIBLE @@ -653,7 +667,7 @@ class TrackCollectionFragment : playNextButton!!.visibility = View.GONE playLastButton!!.visibility = View.GONE - if (listSize == 0 || list.size < listSize) { + if (listSize == 0 || entryList.size < listSize) { albumButtons!!.visibility = View.GONE } else { moreButton!!.visibility = View.VISIBLE @@ -666,7 +680,7 @@ class TrackCollectionFragment : Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE ) - playAllButtonVisible = !(isAlbumList || list.isEmpty()) && !allVideos + playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos shareButtonVisible = !isOffline() && songCount > 0 // listView!!.removeHeaderView(emptyView!!) @@ -684,7 +698,18 @@ class TrackCollectionFragment : 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 = mutableListOf(albumHeader) + mixedList.addAll(entryList) + viewAdapter.submitList(mixedList) + } else { + viewAdapter.submitList(entryList) + } + val playAll = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) if (playAll && songCount > 0) { @@ -699,77 +724,10 @@ class TrackCollectionFragment : } - private fun createHeader( - entries: List, - name: CharSequence?, - songCount: Int - ): View? { - val coverArtView = header!!.findViewById(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(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(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(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(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(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(R.id.select_album_duration) - durationView.text = duration - - return header - } - private fun getSelectedSongs(): MutableList { val songs: MutableList = mutableListOf() - for (i in 0 until listView!!.childCount) { - val vh = listView!!.findViewHolderForAdapterPosition(i) as TrackViewHolder? ?: continue - + for (vh in viewHolders) { if (vh.isChecked) { songs.add(vh.entry!!) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index d84fb080..c35f4503 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -871,7 +871,7 @@ object Util { val descriptionBuilder = MediaDescriptionCompat.Builder() val desc = readableEntryDescription(song) - var title = "" + val title: String if (groupNameId != null) descriptionBuilder.setExtras( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt index 020b9ac9..6a9c0db5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt @@ -1,316 +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, -// ) { +//package org.moire.ultrasonic.view // -// 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 -> { -// 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 -// } +///** +// * 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 // } // -// // TODO: Migrate the image animation stuff from SongView into this class +// check.isVisible = (checkable && !song.isVideo) +// drag.isVisible = draggable // -// if (image != null) { -// status.setCompoundDrawablesWithIntrinsicBounds( -// image, null, null, null +// 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 // ) -// } // -// if (image === DownloadRowAdapter.downloadingImage) { -// // FIXME -//// val frameAnimation = image as AnimationDrawable -//// -//// frameAnimation.setVisible(true, true) -//// frameAnimation.start() +// 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() +// } // } // } - - 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 - } - - -} \ No newline at end of file +// +// +// @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 +// } +// +// +//} \ No newline at end of file From d0e39efc50024469b3fc1ccfd69e9e9bcbf4bce9 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 14 Nov 2021 21:20:23 +0100 Subject: [PATCH 03/33] Migrate DownloadsFragment to new system --- .../fragment/BookmarksFragment.java | 4 +- .../org/moire/ultrasonic/util/AlbumHeader.kt | 2 +- .../adapters/MultiTypeDiffAdapter.kt | 61 ++++++-- .../ultrasonic/adapters/TrackViewBinder.kt | 44 +++--- .../ultrasonic/adapters/TrackViewHolder.kt | 15 +- .../ultrasonic/fragment/DownloadsFragment.kt | 35 ++--- .../ultrasonic/fragment/MultiListFragment.kt | 13 +- .../fragment/ServerSelectorFragment.kt | 1 - .../fragment/TrackCollectionFragment.kt | 147 +++++++----------- .../fragment/TrackCollectionModel.kt | 1 - .../moire/ultrasonic/service/DownloadFile.kt | 3 + ultrasonic/src/main/res/values-cs/strings.xml | 1 - ultrasonic/src/main/res/values-de/strings.xml | 3 +- ultrasonic/src/main/res/values-es/strings.xml | 1 - ultrasonic/src/main/res/values-fr/strings.xml | 1 - ultrasonic/src/main/res/values-hu/strings.xml | 1 - ultrasonic/src/main/res/values-it/strings.xml | 1 - ultrasonic/src/main/res/values-nl/strings.xml | 1 - ultrasonic/src/main/res/values-pl/strings.xml | 1 - .../src/main/res/values-pt-rBR/strings.xml | 1 - ultrasonic/src/main/res/values-pt/strings.xml | 1 - ultrasonic/src/main/res/values-ru/strings.xml | 1 - .../src/main/res/values-zh-rCN/strings.xml | 1 - ultrasonic/src/main/res/values/strings.xml | 23 ++- 24 files changed, 175 insertions(+), 188 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java index 36897f5f..9375de80 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java @@ -226,10 +226,10 @@ public class BookmarksFragment extends Fragment { } } - // Display toast: N tracks selected / N tracks unselected + // Display toast: N tracks selected 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)); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt index 522e94ef..7b57bf29 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt @@ -89,7 +89,7 @@ class AlbumHeader( get() = "HEADER" override val longId: Long - get() = id.hashCode().toLong() + get() = -1L override fun compareTo(other: Identifiable): Int { return this.longId.compareTo(other.longId) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt index 7bb0e251..31de8366 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt @@ -1,24 +1,23 @@ 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.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData 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 +import java.util.TreeSet class MultiTypeDiffAdapter : MultiTypeAdapter() { - val diffCallback = GenericDiffCallback() - var tracker: SelectionTracker? = null + internal var selectedSet: TreeSet = TreeSet() + internal var selectionRevision: MutableLiveData = MutableLiveData(0) + + private val diffCallback = GenericDiffCallback() init { setHasStableIds(true) @@ -28,10 +27,14 @@ class MultiTypeDiffAdapter : MultiTypeAdapter() { return getItem(position).longId } + private fun getItem(position: Int): T { + return mDiffer.currentList[position] + } + override var items: List get() = getCurrentList() 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 : MultiTypeAdapter() { mDiffer.submitList(list, commitCallback) } - protected fun getItem(position: Int): T { - return mDiffer.currentList[position] - } + override fun getItemCount(): Int { return mDiffer.currentList.size @@ -130,8 +131,42 @@ class MultiTypeDiffAdapter : MultiTypeAdapter() { // 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 { @@ -150,8 +185,6 @@ class MultiTypeDiffAdapter : MultiTypeAdapter() { } - - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index 1533f67c..d79fb10c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -3,6 +3,7 @@ package org.moire.ultrasonic.adapters import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup +import androidx.lifecycle.LifecycleOwner import com.drakeet.multitype.ItemViewBinder import org.koin.core.component.KoinComponent 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.service.DownloadFile import org.moire.ultrasonic.service.Downloader +import timber.log.Timber class TrackViewBinder( - val selectedSet: MutableSet, val checkable: Boolean, val draggable: Boolean, - context: Context + context: Context, + val lifecycleOwner: LifecycleOwner ) : ItemViewBinder(), KoinComponent { @@ -35,12 +37,10 @@ class TrackViewBinder( 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) + return TrackViewHolder(inflater.inflate(layout, parent, false), adapter as MultiTypeDiffAdapter) } override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) { @@ -64,23 +64,29 @@ class TrackViewBinder( holder.setSong( file = downloadFile, 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 -// item.status.observe( -// lifecycleOwner, -// { -// holder.updateDownloadStatus(item) -// } -// ) -// -// item.progress.observe( -// lifecycleOwner, -// { -// holder.updateDownloadStatus(item) -// } -// ) + downloadFile.status.observe(lifecycleOwner, { + Timber.w("CAUGHT STATUS CHANGE") + holder.updateDownloadStatus(downloadFile) + } + ) + + downloadFile.progress.observe(lifecycleOwner, { + Timber.w("CAUGHT PROGRESS CHANGE") + holder.updateDownloadStatus(downloadFile) + } + ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 116a2852..7b8356ec 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -9,12 +9,14 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.core.view.isVisible +import androidx.lifecycle.findViewTreeLifecycleOwner 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.Identifiable import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.featureflags.Feature import org.moire.ultrasonic.featureflags.FeatureStorage @@ -29,8 +31,9 @@ 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) : +class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter) : 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) @@ -99,6 +102,7 @@ class TrackViewHolder(val view: View, val selectedSet: MutableSet) : } check.isVisible = (checkable && !song.isVideo) + check.isChecked = isSelected drag.isVisible = draggable if (ActiveServerProvider.isOffline()) { @@ -109,9 +113,6 @@ class TrackViewHolder(val view: View, val selectedSet: MutableSet) : } update() - - isChecked = isSelected - } private fun setupStarButtons(song: MusicDirectory.Entry) { @@ -219,7 +220,6 @@ class TrackViewHolder(val view: View, val selectedSet: MutableSet) : } fun updateDownloadStatus(downloadFile: DownloadFile) { - if (downloadFile.isWorkDone) { val newLeftImageType = if (downloadFile.isSaved) ImageType.Pin else ImageType.Downloaded @@ -274,10 +274,9 @@ class TrackViewHolder(val view: View, val selectedSet: MutableSet) : override fun setChecked(newStatus: Boolean) { if (newStatus) { - selectedSet.add(downloadFile!!.longId) - Timber.d("Selectedset %s", selectedSet.toString()) + adapter.notifySelected(downloadFile!!.longId) } else { - selectedSet.remove(downloadFile!!.longId) + adapter.notifyUnselected(downloadFile!!.longId) } check.isChecked = newStatus } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index 0af78788..209c268b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -3,6 +3,7 @@ package org.moire.ultrasonic.fragment import android.app.Application import android.os.Bundle import android.view.MenuItem +import android.view.View import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData 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.Downloader import org.moire.ultrasonic.util.Util -import java.util.TreeSet -class DownloadsFragment : MultiListFragment>() { +class DownloadsFragment : MultiListFragment() { /** * The ViewModel to use to get the data @@ -36,22 +36,6 @@ class DownloadsFragment : MultiListFragment by lazy { - val adapter = MultiTypeDiffAdapter() - adapter.register( - TrackViewBinder( - selectedSet = TreeSet(), - checkable = false, - draggable = false, - context = requireContext() - ) - ) - adapter - } - override fun onContextMenuItemSelected(menuItem: MenuItem, item: DownloadFile): Boolean { // Do nothing return true @@ -64,6 +48,21 @@ class DownloadsFragment : MultiListFragment : Fragment() { +abstract class MultiListFragment : Fragment() { internal val activeServerProvider: ActiveServerProvider by inject() internal val serverSettingsModel: ServerSettingsModel by viewModel() internal val imageLoaderProvider: ImageLoaderProvider by inject() @@ -47,7 +48,9 @@ abstract class MultiListFragment : Frag * The Adapter for the RecyclerView * Recommendation: Implement this as a lazy delegate */ - internal abstract val viewAdapter: TA + internal val viewAdapter: MultiTypeDiffAdapter by lazy { + MultiTypeDiffAdapter() + } /** * The ViewModel to use to get the data @@ -144,9 +147,9 @@ abstract class MultiListFragment : Frag liveDataItems = getLiveData(arguments) // Register an observer to update our UI when the data changes -// liveDataItems.observe(viewLifecycleOwner, { -// newItems -> viewAdapter.submitList(newItems) -// }) + liveDataItems.observe(viewLifecycleOwner, { + newItems -> viewAdapter.submitList(newItems) + }) // Setup the Music folder handling listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt index f06ed156..05d7b568 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt @@ -16,7 +16,6 @@ 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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 1cf42767..767b4299 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -34,7 +34,6 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.HeaderViewBinder 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 @@ -51,7 +50,6 @@ import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util import timber.log.Timber import java.util.Collections -import java.util.TreeSet /** * 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) */ class TrackCollectionFragment : - MultiListFragment>() { + MultiListFragment() { private var albumButtons: View? = null private var emptyView: TextView? = null @@ -86,8 +84,6 @@ class TrackCollectionFragment : override val listModel: TrackCollectionModel by viewModels() - private var selectedSet: TreeSet = TreeSet() - /** * The id of the main layout */ @@ -111,19 +107,6 @@ class 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?) { super.onViewCreated(view, savedInstanceState) cancellationToken = CancellationToken() @@ -136,7 +119,7 @@ class TrackCollectionFragment : updateDisplay(true) } - listModel.currentList.observe(viewLifecycleOwner, defaultObserver) + listModel.currentList.observe(viewLifecycleOwner, updateInterfaceWithEntries) listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver) // listView!!.setOnItemClickListener { parent, theView, position, _ -> @@ -185,9 +168,11 @@ class TrackCollectionFragment : selectButton!!.setOnClickListener { selectAllOrNone() } + playNowButton!!.setOnClickListener { playNow(false) } + playNextButton!!.setOnClickListener { downloadHandler.download( this@TrackCollectionFragment, append = true, @@ -195,18 +180,23 @@ class TrackCollectionFragment : songs = getSelectedSongs() ) } + playLastButton!!.setOnClickListener { playNow(true) } + pinButton!!.setOnClickListener { downloadBackground(true) } + unpinButton!!.setOnClickListener { unpin() } + downloadButton!!.setOnClickListener { downloadBackground(false) } + deleteButton!!.setOnClickListener { delete() } @@ -214,7 +204,6 @@ class TrackCollectionFragment : registerForContextMenu(listView!!) setHasOptionsMenu(true) - // Create a View Manager viewManager = LinearLayoutManager(this.context) @@ -225,7 +214,6 @@ class TrackCollectionFragment : adapter = viewAdapter } - viewAdapter.register( HeaderViewBinder( context = requireContext() @@ -234,16 +222,20 @@ class TrackCollectionFragment : viewAdapter.register( TrackViewBinder( - selectedSet = selectedSet, checkable = true, draggable = false, - context = requireContext() + context = requireContext(), + lifecycleOwner = viewLifecycleOwner ) ) - enableButtons() + // Update the buttons when the selection has changed + viewAdapter.selectionRevision.observe(viewLifecycleOwner, { + enableButtons() + }) + // Loads the data updateDisplay(false) } @@ -387,33 +379,24 @@ class TrackCollectionFragment : } } - private val viewHolders: List - get() { - val list: MutableList = mutableListOf() - for (i in 0 until listView!!.childCount) { - val vh = listView!!.findViewHolderForAdapterPosition(i) - if (vh is TrackViewHolder) { - list.add(vh) - } - } - return list - } - + /** + * Get the size of the underlying list + */ private val childCount: Int get() { + val count = viewAdapter.getCurrentList().count() if (listModel.showHeader) { - return listView!!.childCount - 1 + return count - 1 } else { - return listView!!.childCount + return count } } private fun playAll(shuffle: Boolean = false, append: Boolean = false) { var hasSubFolders = false - for (vh in viewHolders) { - val entry = vh.entry - if (entry != null && entry.isDirectory) { + for (item in viewAdapter.getCurrentList()) { + if (item is MusicDirectory.Entry && item.isDirectory) { hasSubFolders = true break } @@ -436,7 +419,6 @@ class TrackCollectionFragment : isArtist = isArtist ) } else { - selectAll(selected = true, toast = false) downloadHandler.download( fragment = this, append = append, @@ -444,49 +426,38 @@ class TrackCollectionFragment : autoPlay = !append, playNext = false, shuffle = shuffle, - songs = getSelectedSongs() + songs = getAllSongs() ) - selectAll(selected = false, toast = false) } } + @Suppress("UNCHECKED_CAST") + private fun getAllSongs(): List { + return viewAdapter.getCurrentList().filter { + it is MusicDirectory.Entry && !it.isDirectory + } as List + } + private fun selectAllOrNone() { - val someUnselected = selectedSet.size < childCount + val someUnselected = viewAdapter.selectedSet.size < childCount selectAll(someUnselected, true) - } private fun selectAll(selected: Boolean, toast: Boolean) { + var selectedCount = viewAdapter.selectedSet.size * -1 - var selectedCount = 0 + selectedCount += viewAdapter.setSelectionStatusOfAll(selected) - listView!! - - 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 + // Display toast: N tracks selected if (toast) { - val toastResId = if (selected) - R.string.select_album_n_selected - else - R.string.select_album_n_unselected - Util.toast(activity, getString(toastResId, selectedCount)) + val toastResId = R.string.select_album_n_selected + Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0))) } - enableButtons() } - private fun enableButtons() { - val selection = getSelectedSongs() + private fun enableButtons(selection: List = getSelectedSongs()) { val enabled = selection.isNotEmpty() var unpinEnabled = false var deleteEnabled = false @@ -517,8 +488,7 @@ class TrackCollectionFragment : var songs = getSelectedSongs() if (songs.isEmpty()) { - selectAll(selected = true, toast = false) - songs = getSelectedSongs() + songs = getAllSongs() } downloadBackground(save, songs) @@ -596,15 +566,12 @@ class TrackCollectionFragment : Navigation.findNavController(requireView()) .navigate(R.id.trackCollectionFragment, bundle) } - - //updateInterfaceWithEntries(musicDirectory) } - private val defaultObserver = Observer(this::updateInterfaceWithEntries) - private fun updateInterfaceWithEntries(newList: List) { + private val updateInterfaceWithEntries = Observer> { - val entryList: MutableList = newList.toMutableList() + val entryList: MutableList = it.toMutableList() if (listModel.currentListIsSortable && Settings.shouldSortByDisc) { Collections.sort(entryList, EntryByDiscAndTrackComparator()) @@ -683,6 +650,7 @@ class TrackCollectionFragment : playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos shareButtonVisible = !isOffline() && songCount > 0 + // TODO!! // listView!!.removeHeaderView(emptyView!!) // if (entries.isEmpty()) { // emptyView!!.text = getString(R.string.select_album_empty) @@ -700,9 +668,9 @@ class TrackCollectionFragment : 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 albumHeader = AlbumHeader(newList, name?: intentAlbumName, songCount) + val albumHeader = AlbumHeader(it, name?: intentAlbumName, songCount) val mixedList: MutableList = mutableListOf(albumHeader) mixedList.addAll(entryList) viewAdapter.submitList(mixedList) @@ -724,26 +692,17 @@ class TrackCollectionFragment : } - private fun getSelectedSongs(): MutableList { - val songs: MutableList = mutableListOf() - - for (vh in viewHolders) { - if (vh.isChecked) { - songs.add(vh.entry!!) - } + private fun getSelectedSongs(): List { + // Walk through selected set and get the Entries based on the saved ids. + return viewAdapter.getCurrentList().mapNotNull { + if (it is MusicDirectory.Entry && viewAdapter.isSelected(it.longId)) + it + else + null } - - for (key in selectedSet) { - songs.add(viewAdapter.getCurrentList().findLast { - it.longId == key - } as MusicDirectory.Entry) - } - return songs } - override val viewAdapter: MultiTypeDiffAdapter by lazy { - MultiTypeDiffAdapter() - } + override fun setTitle(title: String?) { setTitle(this@TrackCollectionFragment, title) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt index 0c547c20..bdee4fe7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt @@ -34,7 +34,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat val currentDirectory: MutableLiveData = MutableLiveData() val currentList: MutableLiveData> = MutableLiveData() val songsForGenre: MutableLiveData = MutableLiveData() - private val downloader: Downloader by inject() suspend fun getMusicFolders(refresh: Boolean) { withContext(Dispatchers.IO) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index 4f0260eb..3a1833f2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -139,6 +139,8 @@ class DownloadFile( Util.delete(completeFile) Util.delete(saveFile) + status.postValue(DownloadStatus.IDLE) + Util.scanMedia(saveFile) } @@ -150,6 +152,7 @@ class DownloadFile( saveFile.name, completeFile.name ) } + status.postValue(DownloadStatus.DONE) } } diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index 071e0266..9a2f3de2 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -130,7 +130,6 @@ Hledat Média nenalezena %d skladeb označeno. - %d skladeb odznačeno. Varování: Připojení nedostupné. Chyba: SD karta nedostupná. Přehrát vše diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index 0182cf3e..a07f2a8c 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -128,8 +128,7 @@ Titel Suche Keine Medien gefunden - %d Titel ausgewählt. - %d Titel abgewählt. + %d Titel ausgewählt Warnung: kein Netz. Fehler: Keine SD Karte verfügbar. Alles wiedergeben diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index c176ee3f..275d4f08 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -145,7 +145,6 @@ Buscar No se han encontrado medios %d pista(s) seleccionada(s). - %d pista(s) deseleccionada(s). Atención: No hay red disponible. Error: No hay tarjeta SD disponible. Reproducir todo diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index 9b75c926..a05419da 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -142,7 +142,6 @@ Recherche Aucun titre trouvé %d pistes sélectionnées. - %d pistes non sélectionnés. Avertissement : Aucun réseau disponible. Erreur : Aucune carte SD disponible. Tout jouer diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index f1aa2a85..b2e6f44c 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -140,7 +140,6 @@ Keresés Nem található média! %d dal kijelölve. - %d dal visszavonva. Figyelem: Hálózat nem áll rendelkezésre! Hiba: SD kártya nem áll rendelkezésre! Összes lejátszása diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index a37ab73c..7a8f4486 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -126,7 +126,6 @@ Cerca Nessun media trovato %dtracce selezionate. - %d tracce non selezionate. Attenzione: nessuna rete disponibile. Errore: Nessuna memoria SD disponibile. Riproduci tutto diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index 0876e987..43a01e14 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -145,7 +145,6 @@ Zoeken Geen media gevonden %d nummers geselecteerd. - %d nummers gedeselecteerd. Waarschuwing: geen internetverbinding. Fout: geen SD-kaart beschikbaar. Alles afspelen diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index e794e991..3a2f4554 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -128,7 +128,6 @@ Wyszukiwanie Brak mediów Zaznaczono %d utworów. - Odznaczono %d utworów. Uwaga: sieć niedostępna. Błąd: Niedostępna karta SD. Odtwórz wszystkie diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index 41dd11fa..e35c4c44 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -142,7 +142,6 @@ Pesquisar Nenhuma mídia encontrada %d faixas selecionadas. - %d faixas desselecionadas. Aviso: Nenhuma rede disponível. Erro: Nenhum cartão SD disponível. Tocar Tudo diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index 3706c9ee..f883e96c 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -128,7 +128,6 @@ Pesquisar Nenhuma mídia encontrada %d faixas selecionadas. - %d faixas desselecionadas. Aviso: Nenhuma rede disponível. Erro: Nenhum cartão SD disponível. Tocar Tudo diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 2c4091ea..2e9e7368 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -142,7 +142,6 @@ Поиск Медиа не найдена %d треки выбраны. - %d треки не выбраны. Предупреждение: сеть недоступна. Ошибка: нет SD-карты Воспроизвести все diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 00860b89..ecdb7d9c 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -141,7 +141,6 @@ 搜索 找不到歌曲 已选择 %d 首曲目。 - 未选择 %d 首曲目。 警告:网络不可用 错误:没有SD卡 播放所有 diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index bbf382a2..5b7746e8 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -146,8 +146,7 @@ Songs Search No media found - %d tracks selected. - %d tracks unselected. + %d tracks selected Warning: No network available. Error: No SD card available. Play All @@ -454,24 +453,24 @@ %d songs - %d song selected to be pinned. - %d songs selected to be pinned. + %d song selected to be pinned + %d songs selected to be pinned - %d song selected to be downloaded. - %d songs selected to be downloaded. + %d song selected to be downloaded + %d songs selected to be downloaded - %d song selected to be unpinned. - %d songs selected to be unpinned. + %d song unpinned + %d songs unpinned - %d song added to the end of play queue. - %d songs added to the end of play queue. + %d song added to the end of play queue + %d songs added to the end of play queue - %d song inserted after current song. - %d songs inserted after current song. + %d song inserted after current song + %d songs inserted after current song %d day left of trial period From 19d014709f3e5407800edbb124313b0f45967b99 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 14 Nov 2021 23:21:53 +0100 Subject: [PATCH 04/33] Don't create DownloadFile instances unnecessarily --- .../moire/ultrasonic/service/DownloadFile.kt | 20 ++++++------- .../moire/ultrasonic/service/Downloader.kt | 30 +++++++++++++------ .../src/main/res/layout/song_details.xml | 4 +-- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index 3a1833f2..271bda33 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -35,8 +35,9 @@ import timber.log.Timber */ class DownloadFile( val song: MusicDirectory.Entry, - private val save: Boolean + save: Boolean ) : KoinComponent, Identifiable { + var shouldSave = save val partialFile: File val completeFile: File private val saveFile: File = FileUtil.getSongFile(song) @@ -114,7 +115,7 @@ class DownloadFile( @get:Synchronized val isWorkDone: Boolean - get() = saveFile.exists() || completeFile.exists() && !save || + get() = saveFile.exists() || completeFile.exists() && !shouldSave || saveWhenDone || completeWhenDone @get:Synchronized @@ -125,10 +126,6 @@ class DownloadFile( val isDownloadCancelled: Boolean get() = downloadTask != null && downloadTask!!.isCancelled - fun shouldSave(): Boolean { - return save - } - fun shouldRetry(): Boolean { return (retryCount > 0) } @@ -188,7 +185,7 @@ class DownloadFile( Util.renameFile(completeFile, saveFile) saveWhenDone = false } else if (completeWhenDone) { - if (save) { + if (shouldSave) { Util.renameFile(partialFile, saveFile) Util.scanMedia(saveFile) } else { @@ -216,11 +213,12 @@ class DownloadFile( if (saveFile.exists()) { Timber.i("%s already exists. Skipping.", saveFile) status.postValue(DownloadStatus.DONE) + Timber.i("UPDATING STATUS") return } if (completeFile.exists()) { - if (save) { + if (shouldSave) { if (isPlaying) { saveWhenDone = true } else { @@ -230,6 +228,7 @@ class DownloadFile( Timber.i("%s already exists. Skipping.", completeFile) } status.postValue(DownloadStatus.DONE) + Timber.i("UPDATING STATUS") return } @@ -252,7 +251,7 @@ class DownloadFile( if (needsDownloading) { // Attempt partial HTTP GET, appending to the file if it exists. val (inStream, partial) = musicService.getDownloadInputStream( - song, partialFile.length(), desiredBitRate, save + song, partialFile.length(), desiredBitRate, shouldSave ) inputStream = inStream @@ -293,7 +292,7 @@ class DownloadFile( if (isPlaying) { completeWhenDone = true } else { - if (save) { + if (shouldSave) { Util.renameFile(partialFile, saveFile) Util.scanMedia(saveFile) } else { @@ -379,6 +378,7 @@ class DownloadFile( private fun setProgress(totalBytesCopied: Long) { if (song.size != null) { progress.postValue((totalBytesCopied * 100 / song.size!!).toInt()) + Timber.i("UPDATING PROGESS") } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 51db86d1..9c8db2a1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -55,6 +55,8 @@ class Downloader( RxBus.playlistPublisher.onNext(playlist) } + var backgroundPriorityCounter = 100 + val downloadChecker = Runnable { try { Timber.w("Checking Downloads") @@ -303,6 +305,8 @@ class Downloader( activelyDownloading.remove(download) } } + + backgroundPriorityCounter = 100 } @Synchronized @@ -327,7 +331,7 @@ class Downloader( @Synchronized fun addToPlaylist( - songs: List, + songs: List, save: Boolean, autoPlay: Boolean, playNext: Boolean, @@ -346,13 +350,13 @@ class Downloader( offset = 0 } for (song in songs) { - val downloadFile = DownloadFile(song!!, save) + val downloadFile = song.getDownloadFile(save) playlist.add(currentPlayingIndex + offset, downloadFile) offset++ } } else { for (song in songs) { - val downloadFile = DownloadFile(song!!, save) + val downloadFile = song.getDownloadFile(save) playlist.add(downloadFile) } } @@ -380,7 +384,10 @@ class Downloader( // Because of the priority handling we add the songs in the reverse order they // were requested, then it is correct in the end. for (song in songs.asReversed()) { - downloadQueue.add(DownloadFile(song, save)) + val file = song.getDownloadFile() + file.shouldSave = save + file.priority = backgroundPriorityCounter++ + downloadQueue.add(file) } checkDownloads() @@ -436,7 +443,7 @@ class Downloader( val size = playlist.size if (size < listSize) { for (song in shufflePlayBuffer[listSize - size]) { - val downloadFile = DownloadFile(song, false) + val downloadFile = song.getDownloadFile(false) playlist.add(downloadFile) playlistUpdateRevision++ } @@ -448,7 +455,7 @@ class Downloader( if (currIndex > SHUFFLE_BUFFER_LIMIT) { val songsToShift = currIndex - 2 for (song in shufflePlayBuffer[songsToShift]) { - playlist.add(DownloadFile(song, false)) + playlist.add(song.getDownloadFile(false)) playlist[0].cancelDownload() playlist.removeAt(0) playlistUpdateRevision++ @@ -475,9 +482,14 @@ class Downloader( const val SHUFFLE_BUFFER_LIMIT = 4 } - // Extension function - fun MusicDirectory.Entry.downloadFile(): DownloadFile { - return getDownloadFileForSong(this) + /** + * Extension function + * Gathers the donwload file for a given song, and modifies shouldSave if provided. + */ + fun MusicDirectory.Entry.getDownloadFile(save: Boolean? = null): DownloadFile { + return getDownloadFileForSong(this).apply { + if (save != null) this.shouldSave = save + } } } diff --git a/ultrasonic/src/main/res/layout/song_details.xml b/ultrasonic/src/main/res/layout/song_details.xml index 4de05f9d..30cb9f5a 100644 --- a/ultrasonic/src/main/res/layout/song_details.xml +++ b/ultrasonic/src/main/res/layout/song_details.xml @@ -17,18 +17,18 @@ a:id="@+id/song_track" a:layout_width="wrap_content" a:layout_height="wrap_content" + a:paddingEnd="6dip" a:layout_gravity="left|center_vertical" a:textAppearance="?android:attr/textAppearanceMedium"/> From 7a2dbf65d92d9525c633cfe8ae8c018b4eaa3b8b Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 14 Nov 2021 23:31:01 +0100 Subject: [PATCH 05/33] Fix jumping downloads in Download view --- .../org/moire/ultrasonic/adapters/TrackViewHolder.kt | 10 ++++------ .../kotlin/org/moire/ultrasonic/service/Downloader.kt | 5 ++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 7b8356ec..a7547b46 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -219,17 +219,15 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter, save: Boolean) { - // Because of the priority handling we add the songs in the reverse order they - // were requested, then it is correct in the end. - for (song in songs.asReversed()) { + // By using the counter we ensure that the songs are added in the correct order + for (song in songs) { val file = song.getDownloadFile() file.shouldSave = save file.priority = backgroundPriorityCounter++ From 6277ee73c0b371af973770d04ec6ae14798760fe Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 15 Nov 2021 00:19:47 +0100 Subject: [PATCH 06/33] Simplify and fix download status display --- .../moire/ultrasonic/adapters/ImageHelper.kt | 2 + .../adapters/MultiTypeDiffAdapter.kt | 9 +- .../ultrasonic/adapters/TrackViewBinder.kt | 5 +- .../ultrasonic/adapters/TrackViewHolder.kt | 101 +++++++++--------- .../moire/ultrasonic/service/DownloadFile.kt | 45 ++++++-- .../res/drawable/ic_baseline_error_dark.xml | 9 ++ .../res/drawable/ic_baseline_error_light.xml | 9 ++ .../src/main/res/layout/song_details.xml | 2 +- ultrasonic/src/main/res/values/styles.xml | 1 + ultrasonic/src/main/res/values/themes.xml | 3 + 10 files changed, 118 insertions(+), 68 deletions(-) create mode 100644 ultrasonic/src/main/res/drawable/ic_baseline_error_dark.xml create mode 100644 ultrasonic/src/main/res/drawable/ic_baseline_error_light.xml diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt index 174c449c..2ce33a51 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt @@ -11,6 +11,7 @@ import org.moire.ultrasonic.util.Util */ class ImageHelper(context: Context) { + lateinit var errorImage: Drawable lateinit var starHollowDrawable: Drawable lateinit var starDrawable: Drawable lateinit var pinImage: Drawable @@ -39,6 +40,7 @@ class ImageHelper(context: Context) { starDrawable = Util.getDrawableFromAttribute(context, R.attr.star_full) pinImage = Util.getDrawableFromAttribute(context, R.attr.pin) downloadedImage = Util.getDrawableFromAttribute(context, R.attr.downloaded) + errorImage = Util.getDrawableFromAttribute(context, R.attr.error) downloadingImage = Util.getDrawableFromAttribute(context, R.attr.downloading) playingImage = Util.getDrawableFromAttribute(context, R.attr.media_play_small) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt index 31de8366..0a40fbc5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt @@ -1,7 +1,6 @@ package org.moire.ultrasonic.adapters import android.annotation.SuppressLint -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.AdapterListUpdateCallback import androidx.recyclerview.widget.AsyncDifferConfig @@ -145,6 +144,14 @@ class MultiTypeDiffAdapter : MultiTypeAdapter() { selectionRevision.postValue(selectionRevision.value!! + 1) } + fun notifyChanged() { + // When the download state of an entry was changed by an external process, + // increase the revision counter in order to update the UI + + selectionRevision.postValue(selectionRevision.value!! + 1) + } + + fun setSelectionStatusOfAll(select: Boolean): Int { // Clear current selection selectedSet.clear() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index d79fb10c..54c5cb12 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -78,13 +78,14 @@ class TrackViewBinder( // Observe download status downloadFile.status.observe(lifecycleOwner, { Timber.w("CAUGHT STATUS CHANGE") - holder.updateDownloadStatus(downloadFile) + holder.updateStatus(it) + holder.adapter.notifyChanged() } ) downloadFile.progress.observe(lifecycleOwner, { Timber.w("CAUGHT PROGRESS CHANGE") - holder.updateDownloadStatus(downloadFile) + holder.updateProgress(it) } ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index a7547b46..d455d630 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -9,7 +9,6 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.core.view.isVisible -import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.recyclerview.widget.RecyclerView import org.koin.core.component.KoinComponent import org.koin.core.component.get @@ -21,6 +20,7 @@ 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.DownloadStatus import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.Settings @@ -47,7 +47,7 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter { + statusImage = imageHelper.downloadedImage + progress.text = null + } + DownloadStatus.PINNED -> { + statusImage = imageHelper.pinImage + progress.text = null + } + DownloadStatus.FAILED, + DownloadStatus.ABORTED -> { + statusImage = imageHelper.errorImage + progress.text = null + } + DownloadStatus.DOWNLOADING -> { + statusImage = imageHelper.downloadingImage + } + else -> { + statusImage = null } - } else { - leftImageType = ImageType.None - leftImage = null } - val rightImageType: ImageType - val rightImage: Drawable? + updateImages() + } - 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 + fun updateProgress(p: Int) { + if (cachedStatus == DownloadStatus.DOWNLOADING) { + progress.text = Util.formatPercentage(p) + } else { + progress.text = null } + } - if (previousLeftImageType != leftImageType || previousRightImageType != rightImageType) { - previousLeftImageType = leftImageType - previousRightImageType = rightImageType + private fun updateImages() { + progress.setCompoundDrawablesWithIntrinsicBounds( + null, null, statusImage, null + ) - status.setCompoundDrawablesWithIntrinsicBounds( - leftImage, null, rightImage, null - ) - - if (rightImage === imageHelper.downloadingImage) { - // FIXME - val frameAnimation = rightImage as AnimationDrawable? - - frameAnimation?.setVisible(true, true) - frameAnimation?.start() - } + if (statusImage === imageHelper.downloadingImage) { + val frameAnimation = statusImage as AnimationDrawable? + frameAnimation?.setVisible(true, true) + frameAnimation?.start() } } @@ -293,11 +295,4 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter = MutableLiveData(0) - val status: MutableLiveData = MutableLiveData(DownloadStatus.IDLE) + val status: MutableLiveData init { + val state: DownloadStatus + partialFile = File(saveFile.parent, FileUtil.getPartialFile(saveFile.name)) completeFile = File(saveFile.parent, FileUtil.getCompleteFile(saveFile.name)) + + when { + saveFile.exists() -> { + state = DownloadStatus.PINNED + } + completeFile.exists() -> { + state = DownloadStatus.DONE + } + else -> { + state = DownloadStatus.IDLE + } + } + + status = MutableLiveData(state) + } /** @@ -143,13 +165,14 @@ class DownloadFile( fun unpin() { if (saveFile.exists()) { - if (!saveFile.renameTo(completeFile)) { + if (saveFile.renameTo(completeFile)) { + status.postValue(DownloadStatus.DONE) + } else { Timber.w( "Renaming file failed. Original file: %s; Rename to: %s", saveFile.name, completeFile.name ) } - status.postValue(DownloadStatus.DONE) } } @@ -212,23 +235,23 @@ class DownloadFile( try { if (saveFile.exists()) { Timber.i("%s already exists. Skipping.", saveFile) - status.postValue(DownloadStatus.DONE) - Timber.i("UPDATING STATUS") + status.postValue(DownloadStatus.PINNED) return } if (completeFile.exists()) { + var newStatus: DownloadStatus = DownloadStatus.DONE if (shouldSave) { if (isPlaying) { saveWhenDone = true } else { Util.renameFile(completeFile, saveFile) + newStatus = DownloadStatus.PINNED } } else { Timber.i("%s already exists. Skipping.", completeFile) } - status.postValue(DownloadStatus.DONE) - Timber.i("UPDATING STATUS") + status.postValue(newStatus) return } @@ -285,8 +308,6 @@ class DownloadFile( } downloadAndSaveCoverArt() - - status.postValue(DownloadStatus.DONE) } if (isPlaying) { @@ -294,9 +315,11 @@ class DownloadFile( } else { if (shouldSave) { Util.renameFile(partialFile, saveFile) + status.postValue(DownloadStatus.PINNED) Util.scanMedia(saveFile) } else { Util.renameFile(partialFile, completeFile) + status.postValue(DownloadStatus.DONE) } } } catch (all: Exception) { @@ -378,7 +401,6 @@ class DownloadFile( private fun setProgress(totalBytesCopied: Long) { if (song.size != null) { progress.postValue((totalBytesCopied * 100 / song.size!!).toInt()) - Timber.i("UPDATING PROGESS") } } @@ -414,6 +436,7 @@ class DownloadFile( override val id: String get() = song.id + override val longId: Long by lazy { id.hashCode().toLong() } @@ -424,5 +447,5 @@ class DownloadFile( } enum class DownloadStatus { - IDLE, DOWNLOADING, RETRYING, FAILED, ABORTED, DONE + IDLE, DOWNLOADING, RETRYING, FAILED, ABORTED, DONE, PINNED, UNKNOWN } diff --git a/ultrasonic/src/main/res/drawable/ic_baseline_error_dark.xml b/ultrasonic/src/main/res/drawable/ic_baseline_error_dark.xml new file mode 100644 index 00000000..4c9185e3 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/ic_baseline_error_dark.xml @@ -0,0 +1,9 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/ic_baseline_error_light.xml b/ultrasonic/src/main/res/drawable/ic_baseline_error_light.xml new file mode 100644 index 00000000..2f22d456 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/ic_baseline_error_light.xml @@ -0,0 +1,9 @@ + + + diff --git a/ultrasonic/src/main/res/layout/song_details.xml b/ultrasonic/src/main/res/layout/song_details.xml index 30cb9f5a..2b592b0a 100644 --- a/ultrasonic/src/main/res/layout/song_details.xml +++ b/ultrasonic/src/main/res/layout/song_details.xml @@ -39,7 +39,7 @@ a:layout_height="wrap_content" a:layout_gravity="right|center_vertical" a:drawablePadding="6dip" - a:paddingEnd="6dip"/> + a:paddingEnd="12dip"/> + diff --git a/ultrasonic/src/main/res/values/themes.xml b/ultrasonic/src/main/res/values/themes.xml index 4e23d827..bf6a0b1b 100644 --- a/ultrasonic/src/main/res/values/themes.xml +++ b/ultrasonic/src/main/res/values/themes.xml @@ -34,6 +34,7 @@ @drawable/ic_menu_share_dark @drawable/ic_menu_download_dark @drawable/stat_sys_download_anim_0_dark + @drawable/ic_baseline_error_dark @drawable/stat_sys_download_dark @drawable/media_backward_normal_dark @drawable/media_forward_normal_dark @@ -99,6 +100,7 @@ @drawable/ic_menu_share_dark @drawable/ic_menu_download_dark @drawable/stat_sys_download_anim_0_dark + @drawable/ic_baseline_error_dark @drawable/stat_sys_download_dark @drawable/media_backward_normal_dark @drawable/media_forward_normal_dark @@ -163,6 +165,7 @@ @drawable/ic_menu_share_light @drawable/ic_menu_download_light @drawable/stat_sys_download_anim_0_light + @drawable/ic_baseline_error_light @drawable/stat_sys_download_light @drawable/media_backward_normal_light @drawable/media_forward_normal_light From d243ae1b44e27368bc201d12bb94ac550c97cfca Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 15 Nov 2021 20:01:04 +0100 Subject: [PATCH 07/33] Use RecycleView inside PlayerFragment --- dependencies.gradle | 2 - ultrasonic/build.gradle | 1 - .../org/moire/ultrasonic/util/AlbumHeader.kt | 5 +- .../org/moire/ultrasonic/view/AlbumView.java | 2 + .../ultrasonic/view/SongListAdapter.java | 55 ------- .../ultrasonic/adapters/HeaderViewBinder.kt | 10 +- .../moire/ultrasonic/adapters/ImageHelper.kt | 2 +- .../adapters/MultiTypeDiffAdapter.kt | 33 ++-- .../ultrasonic/adapters/ServerRowAdapter.kt | 1 - .../ultrasonic/adapters/TrackViewBinder.kt | 59 +++++--- .../ultrasonic/adapters/TrackViewHolder.kt | 40 ++--- .../adapters/legacy/SongListAdapter.kt | 69 +++++++++ .../ultrasonic/fragment/DownloadsFragment.kt | 8 +- .../ultrasonic/fragment/MultiListFragment.kt | 25 ++- .../ultrasonic/fragment/PlayerFragment.kt | 142 +++++++++++------- .../fragment/TrackCollectionFragment.kt | 57 +++---- .../fragment/TrackCollectionModel.kt | 3 - .../moire/ultrasonic/service/DownloadFile.kt | 1 - .../moire/ultrasonic/service/Downloader.kt | 1 - .../moire/ultrasonic/util/DragSortCallback.kt | 31 ++++ .../kotlin/org/moire/ultrasonic/util/Util.kt | 6 +- .../moire/ultrasonic/view/SongViewHolder.kt | 132 ++++++++-------- .../src/main/res/layout/current_playlist.xml | 15 +- .../src/main/res/layout/song_list_item.xml | 6 +- ultrasonic/src/main/res/values-cs/strings.xml | 2 +- ultrasonic/src/main/res/values-de/strings.xml | 2 +- ultrasonic/src/main/res/values-es/strings.xml | 2 +- ultrasonic/src/main/res/values-fr/strings.xml | 2 +- ultrasonic/src/main/res/values-hu/strings.xml | 2 +- ultrasonic/src/main/res/values-it/strings.xml | 2 +- ultrasonic/src/main/res/values-nl/strings.xml | 2 +- ultrasonic/src/main/res/values-pl/strings.xml | 2 +- .../src/main/res/values-pt-rBR/strings.xml | 2 +- ultrasonic/src/main/res/values-pt/strings.xml | 2 +- ultrasonic/src/main/res/values-ru/strings.xml | 2 +- .../src/main/res/values-zh-rCN/strings.xml | 2 +- ultrasonic/src/main/res/values/strings.xml | 6 +- 37 files changed, 393 insertions(+), 343 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/view/SongListAdapter.java create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/legacy/SongListAdapter.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt diff --git a/dependencies.gradle b/dependencies.gradle index 33ad5dd2..fffa284b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -30,7 +30,6 @@ ext.versions = [ okhttp : "3.12.13", koin : "3.0.2", picasso : "2.71828", - sortListView : "1.0.1", junit4 : "4.13.2", junit5 : "5.8.1", @@ -92,7 +91,6 @@ ext.other = [ dexter : "com.karumi:dexter:$versions.dexter", timber : "com.jakewharton.timber:timber:$versions.timber", fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll", - sortListView : "com.github.tzugen:drag-sort-listview:$versions.sortListView", colorPickerView : "com.github.skydoves:colorpickerview:$versions.colorPicker", rxJava : "io.reactivex.rxjava3:rxjava:$versions.rxJava", rxAndroid : "io.reactivex.rxjava3:rxandroid:$versions.rxAndroid", diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 934e3b0d..6bb8204e 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -104,7 +104,6 @@ dependencies { implementation other.koinAndroid implementation other.okhttpLogging implementation other.fastScroll - implementation other.sortListView implementation other.colorPickerView implementation other.rxJava implementation other.rxAndroid diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt index 7b57bf29..f2a38aa9 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt @@ -1,16 +1,16 @@ package org.moire.ultrasonic.util +import java.util.HashSet 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, var name: String, songCount: Int -): Identifiable { +) : Identifiable { var isAllVideo: Boolean private set @@ -72,7 +72,6 @@ class AlbumHeader( } } - init { _artists = HashSet() _grandParents = HashSet() diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java index 1d77defc..17e85f98 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java @@ -37,6 +37,8 @@ import org.moire.ultrasonic.util.Util; * * @author Sindre Mehus */ + + public class AlbumView extends UpdateView { private static Drawable starDrawable; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongListAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongListAdapter.java deleted file mode 100644 index 32cae84d..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongListAdapter.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.moire.ultrasonic.view; - -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; - -import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.service.DownloadFile; - -import java.util.List; - -public class SongListAdapter extends ArrayAdapter -{ - Context context; - - public SongListAdapter(Context context, final List entries) - { - super(context, android.R.layout.simple_list_item_1, entries); - this.context = context; - } - - @Override - public View getView(final int position, final View convertView, final ViewGroup parent) - { - DownloadFile downloadFile = getItem(position); - MusicDirectory.Entry entry = downloadFile.getSong(); - - SongView view; - - if (convertView instanceof SongView) - { - SongView currentView = (SongView) convertView; - if (currentView.getEntry().equals(entry)) - { - currentView.update(); - return currentView; - } - else - { - EntryAdapter.SongViewHolder viewHolder = (EntryAdapter.SongViewHolder) convertView.getTag(); - view = currentView; - view.setViewHolder(viewHolder); - } - } - else - { - view = new SongView(this.context); - view.setLayout(entry); - } - - view.setSong(entry, false, true); - return view; - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt index 4b3f6fd0..9a82885d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt @@ -8,15 +8,14 @@ import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.drakeet.multitype.ItemViewBinder +import java.lang.ref.WeakReference +import java.util.Random 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 @@ -51,7 +50,6 @@ class HeaderViewBinder( val context = weakContext.get() ?: return val resources = context.resources - val artworkSelection = random.nextInt(item.childCount) imageLoaderProvider.getImageLoader().loadImage( @@ -61,7 +59,6 @@ class HeaderViewBinder( holder.titleView.text = item.name - // Don't show a header if all entries are videos if (item.isAllVideo) { return @@ -74,7 +71,6 @@ class HeaderViewBinder( } holder.artistView.text = artist - val genre: String = if (item.genres.size == 1) { item.genres.iterator().next() } else { @@ -83,7 +79,6 @@ class HeaderViewBinder( holder.genreView.text = genre - val year: String = if (item.years.size == 1) { item.years.iterator().next().toString() } else { @@ -92,7 +87,6 @@ class HeaderViewBinder( holder.yearView.text = year - val songs = resources.getQuantityString( R.plurals.select_album_n_songs, item.childCount, item.childCount diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt index 2ce33a51..18982f5f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt @@ -25,7 +25,7 @@ class ImageHelper(context: Context) { val themesMatch = theme == currentTheme if (!themesMatch) theme = currentTheme - if (!themesMatch || force ) { + if (!themesMatch || force) { getDrawables(context) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt index 0a40fbc5..86929dd6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt @@ -8,8 +8,8 @@ import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer.ListListener import androidx.recyclerview.widget.DiffUtil import com.drakeet.multitype.MultiTypeAdapter -import org.moire.ultrasonic.domain.Identifiable import java.util.TreeSet +import org.moire.ultrasonic.domain.Identifiable class MultiTypeDiffAdapter : MultiTypeAdapter() { @@ -36,7 +36,6 @@ class MultiTypeDiffAdapter : MultiTypeAdapter() { throw IllegalAccessException("You must use submitList() to add data to the MultiTypeDiffAdapter") } - var mDiffer: AsyncListDiffer = AsyncListDiffer( AdapterListUpdateCallback(this), AsyncDifferConfig.Builder(diffCallback).build() @@ -54,7 +53,6 @@ class MultiTypeDiffAdapter : MultiTypeAdapter() { mDiffer.addListListener(mListener) } - /** * Submits a new list to be diffed, and displayed. * @@ -88,8 +86,6 @@ class MultiTypeDiffAdapter : MultiTypeAdapter() { mDiffer.submitList(list, commitCallback) } - - override fun getItemCount(): Int { return mDiffer.currentList.size } @@ -151,7 +147,6 @@ class MultiTypeDiffAdapter : MultiTypeAdapter() { selectionRevision.postValue(selectionRevision.value!! + 1) } - fun setSelectionStatusOfAll(select: Boolean): Int { // Clear current selection selectedSet.clear() @@ -163,10 +158,13 @@ class MultiTypeDiffAdapter : MultiTypeAdapter() { 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 } - }) + getCurrentList().mapNotNullTo( + selectedSet, + { entry -> + // Exclude any -1 ids, eg. headers and other UI elements + entry.longId.takeIf { it != -1L } + } + ) return selectedSet.count() } @@ -175,6 +173,18 @@ class MultiTypeDiffAdapter : MultiTypeAdapter() { return selectedSet.contains(longId) } + fun moveItem(from: Int, to: Int): List { + val list = getCurrentList().toMutableList() + val fromLocation = list[from] + list.removeAt(from) + if (to < from) { + list.add(to + 1, fromLocation) + } else { + list.add(to - 1, fromLocation) + } + submitList(list) + return list as List + } companion object { /** @@ -190,8 +200,5 @@ class MultiTypeDiffAdapter : MultiTypeAdapter() { return oldItem.id == newItem.id } } - - } - } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt index de8c1590..2b9e4be1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt @@ -18,7 +18,6 @@ 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 /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index 54c5cb12..5fdd494b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -2,6 +2,7 @@ package org.moire.ultrasonic.adapters import android.content.Context import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.lifecycle.LifecycleOwner import com.drakeet.multitype.ItemViewBinder @@ -12,16 +13,15 @@ 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 timber.log.Timber class TrackViewBinder( val checkable: Boolean, val draggable: Boolean, context: Context, - val lifecycleOwner: LifecycleOwner + val lifecycleOwner: LifecycleOwner, + private val onClickCallback: ((View, DownloadFile?) -> Unit)? = null ) : ItemViewBinder(), KoinComponent { - // // // onItemClick: (MusicDirectory.Entry) -> Unit, // onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean, @@ -40,12 +40,13 @@ class TrackViewBinder( private val imageHelper: ImageHelper = ImageHelper(context) override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): TrackViewHolder { - return TrackViewHolder(inflater.inflate(layout, parent, false), adapter as MultiTypeDiffAdapter) + return TrackViewHolder(inflater.inflate(layout, parent, false)) } override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) { val downloadFile: DownloadFile? + val _adapter = adapter as MultiTypeDiffAdapter<*> when (item) { is MusicDirectory.Entry -> { @@ -65,33 +66,47 @@ class TrackViewBinder( file = downloadFile, checkable = checkable, draggable = draggable, - holder.adapter.isSelected(item.longId) + _adapter.isSelected(item.longId) + ) + + // Notify the adapter of selection changes + holder.observableChecked.observe( + lifecycleOwner, + { newValue -> + if (newValue) { + _adapter.notifySelected(item.longId) + } else { + _adapter.notifyUnselected(item.longId) + } + } ) // Listen to changes in selection status and update ourselves - holder.adapter.selectionRevision.observe(lifecycleOwner, { - val newStatus = holder.adapter.isSelected(item.longId) + _adapter.selectionRevision.observe( + lifecycleOwner, + { + val newStatus = _adapter.isSelected(item.longId) - if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus - }) - - // Observe download status - downloadFile.status.observe(lifecycleOwner, { - Timber.w("CAUGHT STATUS CHANGE") - holder.updateStatus(it) - holder.adapter.notifyChanged() + if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus } ) - downloadFile.progress.observe(lifecycleOwner, { - Timber.w("CAUGHT PROGRESS CHANGE") + // Observe download status + downloadFile.status.observe( + lifecycleOwner, + { + holder.updateStatus(it) + _adapter.notifyChanged() + } + ) + + downloadFile.progress.observe( + lifecycleOwner, + { holder.updateProgress(it) } ) + + holder.itemClickListener = onClickCallback } - - } - - - diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index d455d630..68a5c24f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -9,13 +9,13 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.core.view.isVisible +import androidx.lifecycle.MutableLiveData 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.Identifiable import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.featureflags.Feature import org.moire.ultrasonic.featureflags.FeatureStorage @@ -31,8 +31,7 @@ import timber.log.Timber * Used to display songs and videos in a `ListView`. * TODO: Video List item */ -class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter) : - RecyclerView.ViewHolder(view), Checkable, KoinComponent { +class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable, KoinComponent { var check: CheckedTextView = view.findViewById(R.id.song_check) var rating: LinearLayout = view.findViewById(R.id.song_rating) @@ -49,6 +48,8 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter Unit)? = null + var entry: MusicDirectory.Entry? = null private set var downloadFile: DownloadFile? = null @@ -59,6 +60,8 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter 0) { track.text = entryDescription.trackNumber } else { @@ -100,7 +106,7 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter?, + val lifecycleOwner: LifecycleOwner +) : + ArrayAdapter(ctx, android.R.layout.simple_list_item_1, entries!!) { + + val layout = R.layout.song_list_item + private val imageHelper: ImageHelper = ImageHelper(context) + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val downloadFile = getItem(position)!! + var view = convertView + val holder: TrackViewHolder + + if (view == null) { + val inflater = LayoutInflater.from(context) + view = inflater.inflate(layout, parent, false) + } + + if (view?.tag is TrackViewHolder) { + holder = view.tag as TrackViewHolder + } else { + holder = TrackViewHolder(view!!) + view.tag = holder + } + + holder.imageHelper = imageHelper + + holder.setSong( + file = downloadFile, + checkable = false, + draggable = true + ) + + // Observe download status + downloadFile.status.observe( + lifecycleOwner, + { + holder.updateStatus(it) + } + ) + + downloadFile.progress.observe( + lifecycleOwner, + { + holder.updateProgress(it) + } + ) + + return view + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index 209c268b..6199206a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -8,9 +8,7 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import org.koin.core.component.inject import org.moire.ultrasonic.R -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.Downloader import org.moire.ultrasonic.util.Util @@ -54,7 +52,7 @@ class DownloadsFragment : MultiListFragment() { viewAdapter.register( TrackViewBinder( - checkable = true, + checkable = false, draggable = false, context = requireContext(), lifecycleOwner = viewLifecycleOwner @@ -65,7 +63,6 @@ class DownloadsFragment : MultiListFragment() { } } - class DownloadListModel(application: Application) : GenericListModel(application) { private val downloader by inject() @@ -73,6 +70,3 @@ class DownloadListModel(application: Application) : GenericListModel(application return downloader.observableDownloads } } - - - diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index 39f1e9db..49cf0e56 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -8,18 +8,14 @@ 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.adapters.MultiTypeDiffAdapter 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 @@ -32,7 +28,6 @@ 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 : Fragment() { internal val activeServerProvider: ActiveServerProvider by inject() @@ -94,7 +89,7 @@ abstract class MultiListFragment : Fragment() { */ @Suppress("CommentOverPrivateProperty") private val musicFolderObserver = { folders: List -> - //viewAdapter.setFolderList(folders, listModel.activeServer.musicFolderId) + // viewAdapter.setFolderList(folders, listModel.activeServer.musicFolderId) } /** @@ -115,7 +110,7 @@ abstract class MultiListFragment : Fragment() { */ fun showFolderHeader(): Boolean { return listModel.showSelectFolderHeader(arguments) && - !listModel.isOffline() && !Settings.shouldUseId3Tags + !listModel.isOffline() && !Settings.shouldUseId3Tags } open fun setTitle(title: String?) { @@ -147,9 +142,13 @@ abstract class MultiListFragment : Fragment() { liveDataItems = getLiveData(arguments) // Register an observer to update our UI when the data changes - liveDataItems.observe(viewLifecycleOwner, { - newItems -> viewAdapter.submitList(newItems) - }) + liveDataItems.observe( + viewLifecycleOwner, + { + newItems -> + viewAdapter.submitList(newItems) + } + ) // Setup the Music folder handling listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver) @@ -165,7 +164,7 @@ abstract class MultiListFragment : Fragment() { } // Configure whether to show the folder header - //viewAdapter.folderHeaderEnabled = showFolderHeader() + // viewAdapter.folderHeaderEnabled = showFolderHeader() } @Override @@ -187,7 +186,7 @@ abstract class MultiListFragment : Fragment() { abstract fun onItemClick(item: T) } -//abstract class EntryListFragment> : +// abstract class EntryListFragment> : // GenericListFragment() { // @Suppress("LongMethod") // override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean { @@ -284,4 +283,4 @@ abstract class MultiListFragment : Fragment() { // bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist)) // findNavController().navigate(itemClickTarget, bundle) // } -//} +// } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index ef7da49b..e6a9eb21 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -36,8 +36,10 @@ import android.widget.ViewFlipper import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.navigation.Navigation -import com.mobeta.android.dslv.DragSortListView -import com.mobeta.android.dslv.DragSortListView.DragSortListener +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView import io.reactivex.rxjava3.disposables.Disposable import java.text.DateFormat import java.text.SimpleDateFormat @@ -58,9 +60,12 @@ import org.koin.android.ext.android.inject import org.koin.core.component.KoinComponent import org.koin.core.component.get import org.moire.ultrasonic.R +import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter +import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.audiofx.VisualizerController import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.domain.RepeatMode @@ -81,7 +86,6 @@ import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.view.AutoRepeatButton -import org.moire.ultrasonic.view.SongListAdapter import org.moire.ultrasonic.view.VisualizerView import timber.log.Timber @@ -94,6 +98,8 @@ class PlayerFragment : GestureDetector.OnGestureListener, KoinComponent, CoroutineScope by CoroutineScope(Dispatchers.Main) { + + // Settings private var swipeDistance = 0 private var swipeVelocity = 0 private var jukeboxAvailable = false @@ -104,6 +110,7 @@ class PlayerFragment : // Detectors & Callbacks private lateinit var gestureScanner: GestureDetector private lateinit var cancellationToken: CancellationToken + private lateinit var dragTouchHelper: ItemTouchHelper // Data & Services private val networkAndStorageChecker: NetworkAndStorageChecker by inject() @@ -114,6 +121,7 @@ class PlayerFragment : private lateinit var executorService: ScheduledExecutorService private var currentPlaying: DownloadFile? = null private var currentSong: MusicDirectory.Entry? = null + private lateinit var viewManager: LinearLayoutManager private var rxBusSubscription: Disposable? = null private var ioScope = CoroutineScope(Dispatchers.IO) @@ -133,7 +141,7 @@ class PlayerFragment : private lateinit var albumTextView: TextView private lateinit var artistTextView: TextView private lateinit var albumArtImageView: ImageView - private lateinit var playlistView: DragSortListView + private lateinit var playlistView: RecyclerView private lateinit var positionTextView: TextView private lateinit var downloadTrackTextView: TextView private lateinit var downloadTotalDurationTextView: TextView @@ -146,6 +154,10 @@ class PlayerFragment : private lateinit var fullStar: Drawable private lateinit var progressBar: SeekBar + internal val viewAdapter: MultiTypeDiffAdapter by lazy { + MultiTypeDiffAdapter() + } + override fun onCreate(savedInstanceState: Bundle?) { Util.applyTheme(this.context) super.onCreate(savedInstanceState) @@ -322,14 +334,7 @@ class PlayerFragment : override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {} }) - playlistView.setOnItemClickListener { _, _, position, _ -> - networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - launch(CommunicationError.getHandler(context)) { - mediaPlayerController.play(position) - onCurrentChanged() - onSliderProgressChanged() - } - } + initPlaylistDisplay() registerForContextMenu(playlistView) @@ -432,15 +437,12 @@ class PlayerFragment : // Scroll to current playing. private fun scrollToCurrent() { - val adapter = playlistView.adapter - if (adapter != null) { - val count = adapter.count - for (i in 0 until count) { - if (currentPlaying == playlistView.getItemAtPosition(i)) { - playlistView.smoothScrollToPositionFromTop(i, 40) - return - } - } + val index = mediaPlayerController.playList.indexOf(currentPlaying) + + if (index != -1) { + val smoothScroller = LinearSmoothScroller(context) + smoothScroller.targetPosition = index + viewManager.startSmoothScroll(smoothScroller) } } @@ -535,7 +537,7 @@ class PlayerFragment : super.onCreateContextMenu(menu, view, menuInfo) if (view === playlistView) { val info = menuInfo as AdapterContextMenuInfo? - val downloadFile = playlistView.getItemAtPosition(info!!.position) as DownloadFile + val downloadFile = viewAdapter.getCurrentList()[info!!.position] as DownloadFile val menuInflater = requireActivity().menuInflater menuInflater.inflate(R.menu.nowplaying_context, menu) val song: MusicDirectory.Entry? @@ -561,7 +563,7 @@ class PlayerFragment : override fun onContextItemSelected(menuItem: MenuItem): Boolean { val info = menuItem.menuInfo as AdapterContextMenuInfo - val downloadFile = playlistView.getItemAtPosition(info.position) as DownloadFile + val downloadFile = viewAdapter.getCurrentList()[info.position] as DownloadFile return menuItemSelected(menuItem.itemId, downloadFile) || super.onContextItemSelected( menuItem ) @@ -842,43 +844,71 @@ class PlayerFragment : } } + private fun initPlaylistDisplay() { + // Create a View Manager + viewManager = LinearLayoutManager(this.context) + + // Hook up the view with the manager and the adapter + playlistView.apply { + setHasFixedSize(true) + layoutManager = viewManager + adapter = viewAdapter + } + + // Create listener + val listener: ((View, DownloadFile?) -> Unit) = { _, file -> + val list = mediaPlayerController.playList + val index = list.indexOf(file) + mediaPlayerController.play(index) + onCurrentChanged() + onSliderProgressChanged() + } + + viewAdapter.register( + TrackViewBinder( + checkable = false, + draggable = true, + context = requireContext(), + lifecycleOwner = viewLifecycleOwner, + listener + ) + ) + + dragTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0 + ) { + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + + val from = viewHolder.bindingAdapterPosition + val to = target.bindingAdapterPosition + + // FIXME: + // Needs to be changed in the playlist as well... + // Move it in the data set + (recyclerView.adapter as MultiTypeDiffAdapter<*>).moveItem(from, to) + + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + } + } + ) + + dragTouchHelper.attachToRecyclerView(playlistView) + } + private fun onPlaylistChanged() { val mediaPlayerController = mediaPlayerController val list = mediaPlayerController.playList - emptyTextView.setText(R.string.download_empty) - val adapter = SongListAdapter(context, list) - playlistView.adapter = adapter - playlistView.setDragSortListener(object : DragSortListener { - override fun drop(from: Int, to: Int) { - if (from != to) { - val item = adapter.getItem(from) - adapter.remove(item) - adapter.notifyDataSetChanged() - adapter.insert(item, to) - adapter.notifyDataSetChanged() - } - } + emptyTextView.setText(R.string.playlist_empty) - override fun drag(from: Int, to: Int) {} - override fun remove(which: Int) { - - val item = adapter.getItem(which) ?: return - - val currentPlaying = mediaPlayerController.currentPlaying - if (currentPlaying == item) { - mediaPlayerController.next() - } - adapter.remove(item) - adapter.notifyDataSetChanged() - val songRemoved = String.format( - resources.getString(R.string.download_song_removed), - item.song.title - ) - Util.toast(context, songRemoved) - onPlaylistChanged() - onCurrentChanged() - } - }) + viewAdapter.submitList(list) emptyTextView.isVisible = list.isEmpty() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 767b4299..6c3e5c8a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -10,12 +10,10 @@ package org.moire.ultrasonic.fragment import android.os.Bundle import android.os.Handler import android.os.Looper -import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import android.view.ViewGroup import android.widget.AdapterView.AdapterContextMenuInfo import android.widget.ImageView import android.widget.TextView @@ -27,12 +25,12 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.Navigation import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import java.util.Collections 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.HeaderViewBinder -import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.Identifiable @@ -49,7 +47,6 @@ import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util import timber.log.Timber -import java.util.Collections /** * Displays a group of tracks, eg. the songs of an album, of a playlist etc. @@ -106,7 +103,6 @@ class TrackCollectionFragment : // FIXME override val itemClickTarget: Int = R.id.trackCollectionFragment - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) cancellationToken = CancellationToken() @@ -232,9 +228,12 @@ class TrackCollectionFragment : enableButtons() // Update the buttons when the selection has changed - viewAdapter.selectionRevision.observe(viewLifecycleOwner, { - enableButtons() - }) + viewAdapter.selectionRevision.observe( + viewLifecycleOwner, + { + enableButtons() + } + ) // Loads the data updateDisplay(false) @@ -454,7 +453,6 @@ class TrackCollectionFragment : val toastResId = R.string.select_album_n_selected Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0))) } - } private fun enableButtons(selection: List = getSelectedSongs()) { @@ -519,12 +517,14 @@ class TrackCollectionFragment : } private fun delete() { - var songs = getSelectedSongs() + val songs = getSelectedSongs() - if (songs.isEmpty()) { - selectAll(selected = true, toast = false) - songs = getSelectedSongs() - } + Util.toast( + context, + resources.getQuantityString( + R.plurals.select_album_n_songs_deleted, songs.size, songs.size + ) + ) mediaPlayerController.delete(songs) } @@ -544,8 +544,8 @@ class TrackCollectionFragment : // 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 { @@ -568,7 +568,6 @@ class TrackCollectionFragment : } } - private val updateInterfaceWithEntries = Observer> { val entryList: MutableList = it.toMutableList() @@ -577,7 +576,6 @@ class TrackCollectionFragment : Collections.sort(entryList, EntryByDiscAndTrackComparator()) } - var allVideos = true var songCount = 0 @@ -650,14 +648,6 @@ class TrackCollectionFragment : playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos shareButtonVisible = !isOffline() && songCount > 0 - // TODO!! -// 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 } @@ -666,11 +656,10 @@ class TrackCollectionFragment : shareButton!!.isVisible = shareButtonVisible } - if (songCount > 0 && listModel.showHeader) { val name = listModel.currentDirectory.value?.name val intentAlbumName = requireArguments().getString(Constants.INTENT_EXTRA_NAME_NAME, "Name")!! - val albumHeader = AlbumHeader(it, name?: intentAlbumName, songCount) + val albumHeader = AlbumHeader(it, name ?: intentAlbumName, songCount) val mixedList: MutableList = mutableListOf(albumHeader) mixedList.addAll(entryList) viewAdapter.submitList(mixedList) @@ -678,7 +667,6 @@ class TrackCollectionFragment : viewAdapter.submitList(entryList) } - val playAll = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) if (playAll && songCount > 0) { playAll( @@ -688,8 +676,6 @@ class TrackCollectionFragment : } listModel.currentListIsSortable = true - - } private fun getSelectedSongs(): List { @@ -702,8 +688,6 @@ class TrackCollectionFragment : } } - - override fun setTitle(title: String?) { setTitle(this@TrackCollectionFragment, title) } @@ -787,16 +771,11 @@ class TrackCollectionFragment : menuItem: MenuItem, item: MusicDirectory.Entry ): Boolean { - //TODO + // TODO return false } override fun onItemClick(item: MusicDirectory.Entry) { // nothing } - - } - - - diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt index bdee4fe7..69a5b15d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt @@ -13,11 +13,8 @@ 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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index 3be7d310..90c8bf45 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -89,7 +89,6 @@ class DownloadFile( } status = MutableLiveData(state) - } /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 96e63bb6..26e2e6c6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -491,4 +491,3 @@ class Downloader( } } } - diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt new file mode 100644 index 00000000..72e27f54 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt @@ -0,0 +1,31 @@ +package org.moire.ultrasonic.util + +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.DOWN +import androidx.recyclerview.widget.ItemTouchHelper.UP +import androidx.recyclerview.widget.RecyclerView +import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter +import timber.log.Timber + +class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + + val from = viewHolder.bindingAdapterPosition + val to = target.bindingAdapterPosition + + Timber.w("MOVED %s %s", to, from) + + // Move it in the data set + (recyclerView.adapter as MultiTypeDiffAdapter<*>).moveItem(from, to) + + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index c35f4503..6851e09c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -862,7 +862,6 @@ object Util { var fileFormat: String?, ) - fun getMediaDescriptionForEntry( song: MusicDirectory.Entry, mediaId: String? = null, @@ -921,8 +920,8 @@ object Util { if (artistName != null) { if (Settings.shouldDisplayBitrateWithArtist && ( - !bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank() - ) + !bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank() + ) ) { artist.append(artistName).append(" (").append( String.format( @@ -939,7 +938,6 @@ object Util { val trackNumber = song.track ?: 0 - val title = StringBuilder(LINE_LENGTH) if (Settings.shouldShowTrackNumber && trackNumber > 0) { trackText = String.format(Locale.ROOT, "%02d.", trackNumber) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt index 6a9c0db5..51357900 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt @@ -1,37 +1,37 @@ -//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 +// 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 { +// 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) @@ -252,42 +252,42 @@ // } // } // -//// 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() -//// } -//// } +// // 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 @@ -313,4 +313,4 @@ // } // // -//} \ No newline at end of file +// } diff --git a/ultrasonic/src/main/res/layout/current_playlist.xml b/ultrasonic/src/main/res/layout/current_playlist.xml index fcee1849..3e6eeafb 100644 --- a/ultrasonic/src/main/res/layout/current_playlist.xml +++ b/ultrasonic/src/main/res/layout/current_playlist.xml @@ -2,31 +2,22 @@ - + a:fastScrollEnabled="true" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/song_list_item.xml b/ultrasonic/src/main/res/layout/song_list_item.xml index 820bc3bb..74f7aa7c 100644 --- a/ultrasonic/src/main/res/layout/song_list_item.xml +++ b/ultrasonic/src/main/res/layout/song_list_item.xml @@ -15,7 +15,8 @@ a:background="@android:color/transparent" a:focusable="false" a:gravity="center_vertical" - a:src="?attr/drag_vertical" /> + a:src="?attr/drag_vertical" + a:importantForAccessibility="no" /> + a:src="?attr/star_hollow" + a:contentDescription="@string/download.menu_star"/> \ No newline at end of file diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index 9a2f3de2..4ae56ac0 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -43,7 +43,7 @@ Opravdu smazat %1$s Záložka odstraněna. Záložka vytvořena na %s. - Playlist je prázdný + Playlist je prázdný Vzdálené ovládání není povoleno. Povolte jukebox mód v Uživatelském > Nastavení na Subsonic serveru. Vzdálené ovládání vypnuto. Hudba je přehrávána na telefonu. Vzdálené ovládání není dostupné v offline módu. diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index a07f2a8c..e7f62174 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -42,7 +42,7 @@ Möchtest du %1$s löschen Lesezeichen entfernt Lesezeichen gesetzt als %s. - Wiedergabeliste ist leer + Wiedergabeliste ist leer Fernbedienung ist nicht erlaubt. Bitte Jukebox Modus auf dem Subsonic Server in Benutzer > Einstellungen aktivieren. Fernbedienung ausgeschaltet. Musik wird auf dem Telefon wiedergegeben. Fernbedienungs-Modus is Offline nicht verfügbar. diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 275d4f08..f1b3f0b5 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -56,7 +56,7 @@ Quieres eliminar %1$s Marcador eliminado. Marcador añadido a %s. - La lista de reproducción esta vacía + La lista de reproducción esta vacía El control remoto no esta habilitado. Por favor habilita el modo jukebox en Configuración > Usuarios en tu servidor de Subsonic. Control remoto apagado. La música se reproduce en tu dispositivo. Control remoto no disponible en modo fuera de línea. diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index a05419da..8852104f 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -53,7 +53,7 @@ Voulez-vous supprimer %1$s Signet supprimé. Signet ajouté à %s. - La playlist est vide + La playlist est vide La télécommande n\'est pas autorisée. Veuillez activer le mode jukebox dans Utilisateurs > Paramètres à partir de votre serveur Subsonic. Mode jukebox désactivé. La musique est jouée sur l\'appareil. Le mode jukebox n\'est pas disponible en mode déconnecté. diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index b2e6f44c..5e255eed 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -53,7 +53,7 @@ Biztos, hogy törölni akarja? %1$s Könyvjelző eltávolítva. Könyvjelző beállítva %s. - A várólista üres! + A várólista üres! A távvezérlés nem áll rendelkezésre. Kérjük, engedélyezze a Jukebox módot a Felhasználók > Beállítások menüpontban, az Ön Subsonic kiszolgálóján! Távvezérlés kikapcsolása. A zenelejátszás a telefonon történik. A távvezérlés nem lehetséges kapcsolat nélküli módban! diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index 7a8f4486..1a0604dc 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -40,7 +40,7 @@ Vuoi eliminare %1$s Segnalibro rimosso. Segnalibro impostato su %s. - Playlist vuota + Playlist vuota Il controllo remoto non è consentito. Per favore abilita la modalità jukebox nelle Impostazioni > Utente nel server Airsonic. Controllo remoto disattivato. La musica verrà riprodotta sullo smartphone. Il controllo remoto non è disponibile nella modalità offline. diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index 43a01e14..f3ac5d47 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -56,7 +56,7 @@ Wil je %1$s verwijderen? Bladwijzer verwijderd. Bladwijzer ingesteld op %s. - Lege afspeellijst + Lege afspeellijst Afstandsbediening wordt niet ondersteund. Schakel jukebox-modus in op je Subsonic-server via Gebruikers > Instellingen. Afstandsbediening uitgeschakeld; muziek wordt afgespeeld op de telefoon. Afstandsbediening is niet beschikbaar in offline-modus. diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 3a2f4554..65642da7 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -42,7 +42,7 @@ Czy chcesz usunąć %1$s? Zakładka usunięta. Zakładka ustawiona na %s. - Playlista jest pusta + Playlista jest pusta Kontrola pilotem jest niedostępna. Proszę uruchomić tryb jukebox w Użytkownicy > Ustawienia na serwerze Subsonic. Tryb pilota jest wyłączony. Muzyka jest odtwarzana w telefonie. Pilot jest niedostępny w trybie offline. diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index e35c4c44..59eec69d 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -53,7 +53,7 @@ Você quer excluir %1$s Favorito removido. Favorito marcado em %s. - Playlist está vazia + Playlist está vazia Controle remoto não está permitido. Habilite o modo jukebox em Usuário > Configurações no seu servidor Subsonic. Controle remoto desligado. Música tocada no celular. Controle remoto não está disponível no modo offline. diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index f883e96c..e297e3d2 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -42,7 +42,7 @@ Você quer apagar %1$s Favorito removido. Favorito marcado em %s. - Playlist está vazia + Playlist está vazia Controle remoto não está permitido. Habilite o modo jukebox em Usuário > Configurações no seu servidor Subsonic. Controle remoto desligado. Música tocada no celular. Controle remoto não está disponível no modo offline. diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 2e9e7368..1e806a57 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -53,7 +53,7 @@ Вы хотите удалить %1$s Закладка удалена Закладка установлена ​​на %s - Плейлист пустой + Плейлист пустой Пульт дистанционного управления не допускается. Пожалуйста, включите режим музыкального автомата в Пользователи > Настройки на вашем Subsonic сервере. Пульт управления выключен. Музыка играет на телефоне Пульт дистанционного управления недоступен в автономном режиме. diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index ecdb7d9c..07027140 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -53,7 +53,7 @@ 确定要删除 %1$s吗 书签已删除。 书签设置为 %s。 - 空的播放列表 + 空的播放列表 不允许远程控制. 请在您的服务器上的 Users > Settings 打开点唱机模式。 关闭远程控制,音乐将在手机上播放 离线模式不支持远程控制 diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 5b7746e8..4b823f28 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -56,7 +56,7 @@ Do you want to delete %1$s Bookmark removed. Bookmark set at %s. - Playlist is empty + Playlist is empty Remote control is not allowed. Please enable jukebox mode in Users > Settings on your Subsonic server. Turned off remote control. Music is played on phone. Remote control is not available in offline mode. @@ -464,6 +464,10 @@ %d song unpinned %d songs unpinned + + %d song deleted + %d songs deleted + %d song added to the end of play queue %d songs added to the end of play queue From 7640f4c4aa0fd1ffb8ef399a794a02e5c5b4abb7 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 16 Nov 2021 17:58:15 +0100 Subject: [PATCH 08/33] Start migration of Album and Artist --- .../ultrasonic/fragment/AlbumListFragment.kt | 30 ++-- .../ultrasonic/fragment/ArtistListFragment.kt | 21 +-- .../ultrasonic/fragment/DownloadsFragment.kt | 4 +- .../fragment/GenericListFragment.kt | 3 +- .../ultrasonic/fragment/NowPlayingFragment.kt | 5 +- .../fragment/TrackCollectionFragment.kt | 134 +++++++++--------- 6 files changed, 98 insertions(+), 99 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 2070278c..559c721e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -7,7 +7,6 @@ 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 @@ -15,7 +14,7 @@ import org.moire.ultrasonic.util.Constants * Displays a list of Albums from the media library * TODO: Check refresh is working */ -class AlbumListFragment : EntryListFragment() { +class AlbumListFragment : EntryListFragment() { /** * The ViewModel to use to get the data @@ -55,19 +54,20 @@ class AlbumListFragment : EntryListFragment onItemClick(entry) }, - { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, - imageLoaderProvider.getImageLoader(), - onMusicFolderUpdate, - requireContext() - ) - } +// FIXME +// /** +// * Provide the Adapter for the RecyclerView with a lazy delegate +// */ +// override val viewAdapter: AlbumRowAdapter by lazy { +// AlbumRowAdapter( +// liveDataItems.value ?: listOf(), +// { entry -> onItemClick(entry) }, +// { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, +// imageLoaderProvider.getImageLoader(), +// onMusicFolderUpdate, +// requireContext() +// ) +// } val newBundleClone: Bundle get() = arguments?.clone() as Bundle diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 989c4b7b..d0c30d59 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -11,7 +11,7 @@ import org.moire.ultrasonic.util.Constants /** * Displays the list of Artists from the media library */ -class ArtistListFragment : EntryListFragment() { +class ArtistListFragment : EntryListFragment() { /** * The ViewModel to use to get the data @@ -50,13 +50,14 @@ class ArtistListFragment : EntryListFragment() /** * Provide the Adapter for the RecyclerView with a lazy delegate */ - override val viewAdapter: ArtistRowAdapter by lazy { - ArtistRowAdapter( - liveDataItems.value ?: listOf(), - { entry -> onItemClick(entry) }, - { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, - imageLoaderProvider.getImageLoader(), - onMusicFolderUpdate - ) - } + // FIXME +// override val viewAdapter: ArtistRowAdapter by lazy { +// ArtistRowAdapter( +// liveDataItems.value ?: listOf(), +// { entry -> onItemClick(entry) }, +// { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, +// imageLoaderProvider.getImageLoader(), +// onMusicFolderUpdate +// ) +// } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index 6199206a..6acaafff 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -35,12 +35,12 @@ class DownloadsFragment : MultiListFragment() { } override fun onContextMenuItemSelected(menuItem: MenuItem, item: DownloadFile): Boolean { - // Do nothing + // TODO: Add code to enable manipulation of the download list return true } override fun onItemClick(item: DownloadFile) { - // Do nothing + // TODO: Add code to enable manipulation of the download list } override fun setTitle(title: String?) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt index c5df2b4f..befd90e8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt @@ -182,8 +182,7 @@ abstract class GenericListFragment> abstract fun onItemClick(item: T) } -abstract class EntryListFragment> : - GenericListFragment() { +abstract class EntryListFragment : MultiListFragment() { @Suppress("LongMethod") override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean { val isArtist = (item is Artist) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index 93b62077..1171738f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -36,6 +36,7 @@ import timber.log.Timber /** * Contains the mini-now playing information box displayed at the bottom of the screen */ +@Suppress("unused") class NowPlayingFragment : Fragment() { private var downX = 0f @@ -90,13 +91,13 @@ class NowPlayingFragment : Fragment() { if (playerState === PlayerState.PAUSED) { playButton!!.setImageDrawable( getDrawableFromAttribute( - context, R.attr.media_play + requireContext(), R.attr.media_play ) ) } else if (playerState === PlayerState.STARTED) { playButton!!.setImageDrawable( getDrawableFromAttribute( - context, R.attr.media_pause + requireContext(), R.attr.media_pause ) ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 6c3e5c8a..fb86404f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -51,9 +51,7 @@ import timber.log.Timber /** * Displays a group of tracks, eg. the songs of an album, of a playlist etc. * TODO: Move Clickhandler into ViewBinders - * TODO: Migrate Album/artistsRow - * TODO: Wrong count (selectall) - * TODO: Handle updates (playstatus, download status) + * TODO: Fix clikc handlers and context menus etc. */ class TrackCollectionFragment : MultiListFragment() { @@ -255,71 +253,71 @@ class TrackCollectionFragment : Timber.d("onContextItemSelected") val info = menuItem.menuInfo as AdapterContextMenuInfo? ?: return true -// 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 = ArrayList(1) -// entries.add(entry) -// shareHandler.createShare( -// this, entries, refreshListView, -// cancellationToken!! -// ) -// return true -// } -// else -> { -// return super.onContextItemSelected(menuItem) -// } -// } + val entry = viewAdapter.getCurrentList()[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 = ArrayList(1) + entries.add(entry) + shareHandler.createShare( + this, entries, refreshListView, + cancellationToken!! + ) + return true + } + else -> { + return super.onContextItemSelected(menuItem) + } + } return true } From f8a87f7c85841a57156c7cdbabc98f69f51cd534 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 23 Nov 2021 20:38:26 +0100 Subject: [PATCH 09/33] BookmarksFragment is now based on TrackCollectionFragment Also start SearchFragment.kt --- .../moire/ultrasonic/domain/SearchResult.kt | 6 +- .../fragment/BookmarksFragment.java | 387 ------------ .../ultrasonic/fragment/SearchFragment.java | 593 ------------------ .../ultrasonic/fragment/SearchFragment.kt | 555 ++++++++++++++++ .../org/moire/ultrasonic/util/AlbumHeader.kt | 3 +- .../moire/ultrasonic/view/PlaylistView.java | 9 +- ...mView.java => PodcastChannelItemView.java} | 11 +- .../org/moire/ultrasonic/view/ShareView.java | 11 +- .../ultrasonic/activity/NavigationActivity.kt | 2 +- .../{AlbumRowAdapter.kt => AlbumRowBinder.kt} | 80 ++- .../ultrasonic/adapters/ArtistRowAdapter.kt | 106 ---- .../ultrasonic/adapters/ArtistRowBinder.kt | 114 ++++ ...MultiTypeDiffAdapter.kt => BaseAdapter.kt} | 4 +- .../adapters/FolderSelectorBinder.kt | 127 ++++ .../ultrasonic/adapters/GenericRowAdapter.kt | 149 ----- .../ultrasonic/adapters/HeaderViewBinder.kt | 8 +- .../org/moire/ultrasonic/adapters/Helper.kt | 22 + .../ultrasonic/adapters/SectionedAdapter.kt | 18 + .../ultrasonic/adapters/ServerRowAdapter.kt | 1 + .../ultrasonic/adapters/TrackViewBinder.kt | 25 +- .../ultrasonic/adapters/TrackViewHolder.kt | 76 +-- .../di/AppPermanentStorageModule.kt | 2 +- .../ultrasonic/fragment/AlbumListFragment.kt | 37 +- .../ultrasonic/fragment/ArtistListFragment.kt | 48 +- .../ultrasonic/fragment/BookmarksFragment.kt | 66 ++ .../ultrasonic/fragment/DownloadsFragment.kt | 1 + .../ultrasonic/fragment/EditServerFragment.kt | 1 + .../ultrasonic/fragment/EntryListFragment.kt | 140 +++++ .../fragment/GenericListFragment.kt | 281 --------- .../ultrasonic/fragment/MultiListFragment.kt | 157 +---- .../ultrasonic/fragment/PlayerFragment.kt | 8 +- .../fragment/ServerSelectorFragment.kt | 1 + .../fragment/TrackCollectionFragment.kt | 281 ++++----- .../{fragment => model}/AlbumListModel.kt | 55 +- .../{fragment => model}/ArtistListModel.kt | 2 +- .../{fragment => model}/GenericListModel.kt | 23 +- .../moire/ultrasonic/model/SearchListModel.kt | 73 +++ .../ServerSettingsModel.kt | 2 +- .../TrackCollectionModel.kt | 78 +-- .../ultrasonic/service/CachedMusicService.kt | 2 +- .../moire/ultrasonic/service/MusicService.kt | 2 +- .../ultrasonic/service/OfflineMusicService.kt | 2 +- .../org/moire/ultrasonic/service/RxBus.kt | 6 + .../org/moire/ultrasonic/util/Constants.kt | 1 + .../moire/ultrasonic/util/DragSortCallback.kt | 4 +- .../kotlin/org/moire/ultrasonic/util/Util.kt | 3 +- .../src/main/res/layout/album_buttons.xml | 27 +- ultrasonic/src/main/res/layout/search.xml | 2 +- .../src/main/res/layout/select_album.xml | 36 -- .../main/res/navigation/navigation_graph.xml | 4 +- ultrasonic/src/main/res/values/strings.xml | 1 + 51 files changed, 1539 insertions(+), 2114 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt rename ultrasonic/src/main/java/org/moire/ultrasonic/view/{PodcatsChannelItemView.java => PodcastChannelItemView.java} (85%) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/{AlbumRowAdapter.kt => AlbumRowBinder.kt} (60%) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowAdapter.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/{MultiTypeDiffAdapter.kt => BaseAdapter.kt} (97%) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/GenericRowAdapter.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/SectionedAdapter.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => model}/AlbumListModel.kt (66%) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => model}/ArtistListModel.kt (98%) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => model}/GenericListModel.kt (88%) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => model}/ServerSettingsModel.kt (99%) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{fragment => model}/TrackCollectionModel.kt (80%) delete mode 100644 ultrasonic/src/main/res/layout/select_album.xml diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt index 82479b70..11c4c97c 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt @@ -6,7 +6,7 @@ import org.moire.ultrasonic.domain.MusicDirectory.Entry * The result of a search. Contains matching artists, albums and songs. */ data class SearchResult( - val artists: List, - val albums: List, - val songs: List + val artists: List = listOf(), + val albums: List = listOf(), + val songs: List = listOf() ) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java deleted file mode 100644 index 9375de80..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java +++ /dev/null @@ -1,387 +0,0 @@ -package org.moire.ultrasonic.fragment; - -import android.os.Bundle; -import android.util.Pair; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.ListView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.data.ActiveServerProvider; -import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.service.DownloadFile; -import org.moire.ultrasonic.service.MediaPlayerController; -import org.moire.ultrasonic.service.MusicService; -import org.moire.ultrasonic.service.MusicServiceFactory; -import org.moire.ultrasonic.subsonic.ImageLoaderProvider; -import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker; -import org.moire.ultrasonic.subsonic.VideoPlayer; -import org.moire.ultrasonic.util.CancellationToken; -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.FragmentBackgroundTask; -import org.moire.ultrasonic.util.Util; -import org.moire.ultrasonic.view.EntryAdapter; - -import java.util.ArrayList; -import java.util.List; - -import kotlin.Lazy; - -import static org.koin.java.KoinJavaComponent.inject; - -/** - * Lists the Bookmarks available on the server - */ -public class BookmarksFragment extends Fragment { - - private SwipeRefreshLayout refreshAlbumListView; - private ListView albumListView; - private View albumButtons; - private View emptyView; - private ImageView playNowButton; - private ImageView pinButton; - private ImageView unpinButton; - private ImageView downloadButton; - private ImageView deleteButton; - - private final Lazy mediaPlayerController = inject(MediaPlayerController.class); - private final Lazy imageLoader = inject(ImageLoaderProvider.class); - private final Lazy networkAndStorageChecker = inject(NetworkAndStorageChecker.class); - private CancellationToken cancellationToken; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - Util.applyTheme(this.getContext()); - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.select_album, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - cancellationToken = new CancellationToken(); - albumButtons = view.findViewById(R.id.menu_album); - super.onViewCreated(view, savedInstanceState); - - refreshAlbumListView = view.findViewById(R.id.select_album_entries_refresh); - albumListView = view.findViewById(R.id.select_album_entries_list); - - refreshAlbumListView.setOnRefreshListener(() -> { - enableButtons(); - getBookmarks(); - }); - - albumListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); - - albumListView.setOnItemClickListener((parent, view17, position, id) -> { - if (position >= 0) - { - MusicDirectory.Entry entry = (MusicDirectory.Entry) parent.getItemAtPosition(position); - - if (entry != null) - { - if (entry.isVideo()) - { - VideoPlayer.Companion.playVideo(getContext(), entry); - } - else - { - enableButtons(); - } - } - } - }); - - ImageView selectButton = view.findViewById(R.id.select_album_select); - playNowButton = view.findViewById(R.id.select_album_play_now); - ImageView playNextButton = view.findViewById(R.id.select_album_play_next); - ImageView playLastButton = view.findViewById(R.id.select_album_play_last); - pinButton = view.findViewById(R.id.select_album_pin); - unpinButton = view.findViewById(R.id.select_album_unpin); - downloadButton = view.findViewById(R.id.select_album_download); - deleteButton = view.findViewById(R.id.select_album_delete); - ImageView oreButton = view.findViewById(R.id.select_album_more); - emptyView = view.findViewById(R.id.select_album_empty); - - selectButton.setVisibility(View.GONE); - playNextButton.setVisibility(View.GONE); - playLastButton.setVisibility(View.GONE); - oreButton.setVisibility(View.GONE); - - playNowButton.setOnClickListener(view16 -> playNow(getSelectedSongs(albumListView))); - - selectButton.setOnClickListener(view15 -> selectAllOrNone()); - pinButton.setOnClickListener(view14 -> { - downloadBackground(true); - selectAll(false, false); - }); - unpinButton.setOnClickListener(view13 -> { - unpin(); - selectAll(false, false); - }); - downloadButton.setOnClickListener(view12 -> { - downloadBackground(false); - selectAll(false, false); - }); - deleteButton.setOnClickListener(view1 -> { - delete(); - selectAll(false, false); - }); - - registerForContextMenu(albumListView); - FragmentTitle.Companion.setTitle(this, R.string.button_bar_bookmarks); - - enableButtons(); - getBookmarks(); - } - - @Override - public void onDestroyView() { - cancellationToken.cancel(); - super.onDestroyView(); - } - - private void getBookmarks() - { - new LoadTask() - { - @Override - protected MusicDirectory load(MusicService service) throws Exception - { - return Util.getSongsFromBookmarks(service.getBookmarks()); - } - }.execute(); - } - - private void playNow(List songs) - { - if (!getSelectedSongs(albumListView).isEmpty()) - { - int position = songs.get(0).getBookmarkPosition(); - mediaPlayerController.getValue().restore(songs, 0, position, true, true); - selectAll(false, false); - } - } - - private static List getSelectedSongs(ListView albumListView) - { - List songs = new ArrayList<>(10); - - if (albumListView != null) - { - int count = albumListView.getCount(); - for (int i = 0; i < count; i++) - { - if (albumListView.isItemChecked(i)) - { - MusicDirectory.Entry song = (MusicDirectory.Entry) albumListView.getItemAtPosition(i); - if (song != null) songs.add(song); - } - } - } - - return songs; - } - - private void selectAllOrNone() - { - boolean someUnselected = false; - int count = albumListView.getCount(); - - for (int i = 0; i < count; i++) - { - if (!albumListView.isItemChecked(i) && albumListView.getItemAtPosition(i) instanceof MusicDirectory.Entry) - { - someUnselected = true; - break; - } - } - - selectAll(someUnselected, true); - } - - private void selectAll(boolean selected, boolean toast) - { - int count = albumListView.getCount(); - int selectedCount = 0; - - for (int i = 0; i < count; i++) - { - MusicDirectory.Entry entry = (MusicDirectory.Entry) albumListView.getItemAtPosition(i); - if (entry != null && !entry.isDirectory() && !entry.isVideo()) - { - albumListView.setItemChecked(i, selected); - selectedCount++; - } - } - - // Display toast: N tracks selected - if (toast) - { - int toastResId = R.string.select_album_n_selected; - Util.toast(getContext(), getString(toastResId, selectedCount)); - } - - enableButtons(); - } - - private void enableButtons() - { - List selection = getSelectedSongs(albumListView); - boolean enabled = !selection.isEmpty(); - boolean unpinEnabled = false; - boolean deleteEnabled = false; - - int pinnedCount = 0; - - for (MusicDirectory.Entry song : selection) - { - if (song == null) continue; - DownloadFile downloadFile = mediaPlayerController.getValue().getDownloadFileForSong(song); - if (downloadFile.isWorkDone()) - { - deleteEnabled = true; - } - - if (downloadFile.isSaved()) - { - pinnedCount++; - unpinEnabled = true; - } - } - - playNowButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE); - pinButton.setVisibility((enabled && !ActiveServerProvider.Companion.isOffline() && selection.size() > pinnedCount) ? View.VISIBLE : View.GONE); - unpinButton.setVisibility(enabled && unpinEnabled ? View.VISIBLE : View.GONE); - downloadButton.setVisibility(enabled && !deleteEnabled && !ActiveServerProvider.Companion.isOffline() ? View.VISIBLE : View.GONE); - deleteButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE); - } - - private void downloadBackground(final boolean save) - { - List songs = getSelectedSongs(albumListView); - - if (songs.isEmpty()) - { - selectAll(true, false); - songs = getSelectedSongs(albumListView); - } - - downloadBackground(save, songs); - } - - private void downloadBackground(final boolean save, final List songs) - { - Runnable onValid = () -> { - networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); - mediaPlayerController.getValue().downloadBackground(songs, save); - - if (save) - { - Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size())); - } - else - { - Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size())); - } - }; - - onValid.run(); - } - - private void delete() - { - List songs = getSelectedSongs(albumListView); - - if (songs.isEmpty()) - { - selectAll(true, false); - songs = getSelectedSongs(albumListView); - } - - mediaPlayerController.getValue().delete(songs); - } - - private void unpin() - { - List songs = getSelectedSongs(albumListView); - Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size())); - mediaPlayerController.getValue().unpin(songs); - } - - private abstract class LoadTask extends FragmentBackgroundTask> - { - public LoadTask() - { - super(BookmarksFragment.this.getActivity(), true, refreshAlbumListView, cancellationToken); - } - - protected abstract MusicDirectory load(MusicService service) throws Exception; - - @Override - protected Pair doInBackground() throws Throwable - { - MusicService musicService = MusicServiceFactory.getMusicService(); - MusicDirectory dir = load(musicService); - boolean valid = musicService.isLicenseValid(); - return new Pair<>(dir, valid); - } - - @Override - protected void done(Pair result) - { - MusicDirectory musicDirectory = result.first; - List entries = musicDirectory.getChildren(); - - int songCount = 0; - for (MusicDirectory.Entry entry : entries) - { - if (!entry.isDirectory()) - { - songCount++; - } - } - - final int listSize = getArguments() == null? 0 : getArguments().getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0); - - if (songCount > 0) - { - pinButton.setVisibility(View.VISIBLE); - unpinButton.setVisibility(View.VISIBLE); - downloadButton.setVisibility(View.VISIBLE); - deleteButton.setVisibility(View.VISIBLE); - playNowButton.setVisibility(View.VISIBLE); - } - else - { - pinButton.setVisibility(View.GONE); - unpinButton.setVisibility(View.GONE); - downloadButton.setVisibility(View.GONE); - deleteButton.setVisibility(View.GONE); - playNowButton.setVisibility(View.GONE); - - if (listSize == 0 || result.first.getChildren().size() < listSize) - { - albumButtons.setVisibility(View.GONE); - } - } - - enableButtons(); - - emptyView.setVisibility(entries.isEmpty() ? View.VISIBLE : View.GONE); - - albumListView.setAdapter(new EntryAdapter(getContext(), imageLoader.getValue().getImageLoader(), entries, true)); - } - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java deleted file mode 100644 index fd2797c1..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java +++ /dev/null @@ -1,593 +0,0 @@ -package org.moire.ultrasonic.fragment; - -import android.app.Activity; -import android.app.SearchManager; -import android.content.Context; -import android.database.Cursor; -import android.os.Bundle; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ListAdapter; -import android.widget.ListView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.SearchView; -import androidx.fragment.app.Fragment; -import androidx.navigation.Navigation; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import org.jetbrains.annotations.NotNull; -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.data.ActiveServerProvider; -import org.moire.ultrasonic.domain.Artist; -import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.domain.SearchCriteria; -import org.moire.ultrasonic.domain.SearchResult; -import org.moire.ultrasonic.service.MediaPlayerController; -import org.moire.ultrasonic.service.MusicService; -import org.moire.ultrasonic.service.MusicServiceFactory; -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.BackgroundTask; -import org.moire.ultrasonic.util.CancellationToken; -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.MergeAdapter; -import org.moire.ultrasonic.util.FragmentBackgroundTask; -import org.moire.ultrasonic.util.Settings; -import org.moire.ultrasonic.util.Util; -import org.moire.ultrasonic.view.ArtistAdapter; -import org.moire.ultrasonic.view.EntryAdapter; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import kotlin.Lazy; -import timber.log.Timber; - -import static org.koin.java.KoinJavaComponent.inject; - -/** - * Initiates a search on the media library and displays the results - */ -public class SearchFragment extends Fragment { - - private static int DEFAULT_ARTISTS; - private static int DEFAULT_ALBUMS; - private static int DEFAULT_SONGS; - - private ListView list; - - private View artistsHeading; - private View albumsHeading; - private View songsHeading; - private TextView notFound; - private View moreArtistsButton; - private View moreAlbumsButton; - private View moreSongsButton; - private SearchResult searchResult; - private MergeAdapter mergeAdapter; - private ArtistAdapter artistAdapter; - private ListAdapter moreArtistsAdapter; - private EntryAdapter albumAdapter; - private ListAdapter moreAlbumsAdapter; - private ListAdapter moreSongsAdapter; - private EntryAdapter songAdapter; - private SwipeRefreshLayout searchRefresh; - - private final Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); - private final Lazy imageLoaderProvider = inject(ImageLoaderProvider.class); - private final Lazy downloadHandler = inject(DownloadHandler.class); - private final Lazy shareHandler = inject(ShareHandler.class); - private final Lazy networkAndStorageChecker = inject(NetworkAndStorageChecker.class); - private CancellationToken cancellationToken; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - Util.applyTheme(this.getContext()); - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.search, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - cancellationToken = new CancellationToken(); - - FragmentTitle.Companion.setTitle(this, R.string.search_title); - setHasOptionsMenu(true); - - DEFAULT_ARTISTS = Settings.getDefaultArtists(); - DEFAULT_ALBUMS = Settings.getDefaultAlbums(); - DEFAULT_SONGS = Settings.getDefaultSongs(); - - View buttons = LayoutInflater.from(getContext()).inflate(R.layout.search_buttons, list, false); - - if (buttons != null) - { - artistsHeading = buttons.findViewById(R.id.search_artists); - albumsHeading = buttons.findViewById(R.id.search_albums); - songsHeading = buttons.findViewById(R.id.search_songs); - notFound = buttons.findViewById(R.id.search_not_found); - moreArtistsButton = buttons.findViewById(R.id.search_more_artists); - moreAlbumsButton = buttons.findViewById(R.id.search_more_albums); - moreSongsButton = buttons.findViewById(R.id.search_more_songs); - } - - list = view.findViewById(R.id.search_list); - searchRefresh = view.findViewById(R.id.search_entries_refresh); - searchRefresh.setEnabled(false); // TODO: It should be enabled if it is a good feature to refresh search results - - list.setOnItemClickListener(new AdapterView.OnItemClickListener() - { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) - { - if (view == moreArtistsButton) - { - expandArtists(); - } - else if (view == moreAlbumsButton) - { - expandAlbums(); - } - else if (view == moreSongsButton) - { - expandSongs(); - } - else - { - Object item = parent.getItemAtPosition(position); - if (item instanceof Artist) - { - onArtistSelected((Artist) item); - } - else if (item instanceof MusicDirectory.Entry) - { - MusicDirectory.Entry entry = (MusicDirectory.Entry) item; - if (entry.isDirectory()) - { - onAlbumSelected(entry, false); - } - else if (entry.isVideo()) - { - onVideoSelected(entry); - } - else - { - onSongSelected(entry, true); - } - - } - } - } - }); - - registerForContextMenu(list); - - // Fragment was started with a query (e.g. from voice search), try to execute search right away - Bundle arguments = getArguments(); - if (arguments != null) { - String query = arguments.getString(Constants.INTENT_EXTRA_NAME_QUERY); - boolean autoPlay = arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false); - - if (query != null) { - mergeAdapter = new MergeAdapter(); - list.setAdapter(mergeAdapter); - search(query, autoPlay); - return; - } - } - - // Fragment was started from the Menu, create empty list - populateList(); - } - - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - Activity activity = getActivity(); - if (activity == null) return; - SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE); - - inflater.inflate(R.menu.search, menu); - MenuItem searchItem = menu.findItem(R.id.search_item); - final SearchView searchView = (SearchView) searchItem.getActionView(); - searchView.setSearchableInfo(searchManager.getSearchableInfo(getActivity().getComponentName())); - - Bundle arguments = getArguments(); - final boolean autoPlay = arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false); - String query = arguments == null? null : arguments.getString(Constants.INTENT_EXTRA_NAME_QUERY); - // If started with a query, enter it to the searchView - if (query != null) { - searchView.setQuery(query, false); - searchView.clearFocus(); - } - - searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() { - @Override - public boolean onSuggestionSelect(int position) { return true; } - - @Override - public boolean onSuggestionClick(int position) { - Timber.d("onSuggestionClick: %d", position); - Cursor cursor= searchView.getSuggestionsAdapter().getCursor(); - cursor.moveToPosition(position); - String suggestion = cursor.getString(2); // TODO: Try to do something with this magic const -- 2 is the index of col containing suggestion name. - searchView.setQuery(suggestion,true); - return true; - } - }); - - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - Timber.d("onQueryTextSubmit: %s", query); - mergeAdapter = new MergeAdapter(); - list.setAdapter(mergeAdapter); - searchView.clearFocus(); - search(query, autoPlay); - return true; - } - - @Override - public boolean onQueryTextChange(String newText) { return true; } - }); - - searchView.setIconifiedByDefault(false); - searchItem.expandActionView(); - } - - @Override - public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo) - { - super.onCreateContextMenu(menu, view, menuInfo); - if (getActivity() == null) return; - - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; - Object selectedItem = list.getItemAtPosition(info.position); - - boolean isArtist = selectedItem instanceof Artist; - boolean isAlbum = selectedItem instanceof MusicDirectory.Entry && ((MusicDirectory.Entry) selectedItem).isDirectory(); - - MenuInflater inflater = getActivity().getMenuInflater(); - if (!isArtist && !isAlbum) - { - inflater.inflate(R.menu.select_song_context, menu); - } - else - { - inflater.inflate(R.menu.generic_context_menu, menu); - } - - MenuItem shareButton = menu.findItem(R.id.menu_item_share); - MenuItem downloadMenuItem = menu.findItem(R.id.menu_download); - - if (downloadMenuItem != null) - { - downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline()); - } - - if (ActiveServerProvider.Companion.isOffline() || isArtist) - { - if (shareButton != null) - { - shareButton.setVisible(false); - } - } - } - - @Override - public boolean onContextItemSelected(MenuItem menuItem) - { - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo(); - - if (info == null) - { - return true; - } - - Object selectedItem = list.getItemAtPosition(info.position); - - Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null; - MusicDirectory.Entry entry = selectedItem instanceof MusicDirectory.Entry ? (MusicDirectory.Entry) selectedItem : null; - - String entryId = null; - - if (entry != null) - { - entryId = entry.getId(); - } - - String id = artist != null ? artist.getId() : entryId; - - if (id == null) - { - return true; - } - - List songs = new ArrayList<>(1); - - int itemId = menuItem.getItemId(); - if (itemId == R.id.menu_play_now) { - downloadHandler.getValue().downloadRecursively(this, id, false, false, true, false, false, false, false, false); - } else if (itemId == R.id.menu_play_next) { - downloadHandler.getValue().downloadRecursively(this, id, false, true, false, true, false, true, false, false); - } else if (itemId == R.id.menu_play_last) { - downloadHandler.getValue().downloadRecursively(this, id, false, true, false, false, false, false, false, false); - } else if (itemId == R.id.menu_pin) { - downloadHandler.getValue().downloadRecursively(this, id, true, true, false, false, false, false, false, false); - } else if (itemId == R.id.menu_unpin) { - downloadHandler.getValue().downloadRecursively(this, id, false, false, false, false, false, false, true, false); - } else if (itemId == R.id.menu_download) { - downloadHandler.getValue().downloadRecursively(this, id, false, false, false, false, true, false, false, false); - } else if (itemId == R.id.song_menu_play_now) { - if (entry != null) { - songs = new ArrayList<>(1); - songs.add(entry); - downloadHandler.getValue().download(this, false, false, true, false, false, songs); - } - } else if (itemId == R.id.song_menu_play_next) { - if (entry != null) { - songs = new ArrayList<>(1); - songs.add(entry); - downloadHandler.getValue().download(this, true, false, false, true, false, songs); - } - } else if (itemId == R.id.song_menu_play_last) { - if (entry != null) { - songs = new ArrayList<>(1); - songs.add(entry); - downloadHandler.getValue().download(this, true, false, false, false, false, songs); - } - } else if (itemId == R.id.song_menu_pin) { - if (entry != null) { - songs.add(entry); - Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size())); - downloadBackground(true, songs); - } - } else if (itemId == R.id.song_menu_download) { - if (entry != null) { - songs.add(entry); - Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size())); - downloadBackground(false, songs); - } - } else if (itemId == R.id.song_menu_unpin) { - if (entry != null) { - songs.add(entry); - Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size())); - mediaPlayerControllerLazy.getValue().unpin(songs); - } - } else if (itemId == R.id.menu_item_share) { - if (entry != null) { - songs = new ArrayList<>(1); - songs.add(entry); - shareHandler.getValue().createShare(this, songs, searchRefresh, cancellationToken); - } - - return super.onContextItemSelected(menuItem); - } else { - return super.onContextItemSelected(menuItem); - } - - return true; - } - - @Override - public void onDestroyView() { - cancellationToken.cancel(); - super.onDestroyView(); - } - - private void downloadBackground(final boolean save, final List songs) - { - Runnable onValid = new Runnable() - { - @Override - public void run() - { - networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); - mediaPlayerControllerLazy.getValue().downloadBackground(songs, save); - } - }; - - onValid.run(); - } - - private void search(final String query, final boolean autoplay) - { - final int maxArtists = Settings.getMaxArtists(); - final int maxAlbums = Settings.getMaxAlbums(); - final int maxSongs = Settings.getMaxSongs(); - - BackgroundTask task = new FragmentBackgroundTask(getActivity(), true, searchRefresh, cancellationToken) - { - @Override - protected SearchResult doInBackground() throws Throwable - { - SearchCriteria criteria = new SearchCriteria(query, maxArtists, maxAlbums, maxSongs); - MusicService service = MusicServiceFactory.getMusicService(); - return service.search(criteria); - } - - @Override - protected void done(SearchResult result) - { - searchResult = result; - - populateList(); - - if (autoplay) - { - autoplay(); - } - - } - }; - task.execute(); - } - - private void populateList() - { - mergeAdapter = new MergeAdapter(); - - if (searchResult != null) - { - List artists = searchResult.getArtists(); - if (!artists.isEmpty()) - { - mergeAdapter.addView(artistsHeading); - List displayedArtists = new ArrayList<>(artists.subList(0, Math.min(DEFAULT_ARTISTS, artists.size()))); - artistAdapter = new ArtistAdapter(getContext(), displayedArtists); - mergeAdapter.addAdapter(artistAdapter); - if (artists.size() > DEFAULT_ARTISTS) - { - moreArtistsAdapter = mergeAdapter.addView(moreArtistsButton, true); - } - } - - List albums = searchResult.getAlbums(); - if (!albums.isEmpty()) - { - mergeAdapter.addView(albumsHeading); - List displayedAlbums = new ArrayList<>(albums.subList(0, Math.min(DEFAULT_ALBUMS, albums.size()))); - albumAdapter = new EntryAdapter(getContext(), imageLoaderProvider.getValue().getImageLoader(), displayedAlbums, false); - mergeAdapter.addAdapter(albumAdapter); - if (albums.size() > DEFAULT_ALBUMS) - { - moreAlbumsAdapter = mergeAdapter.addView(moreAlbumsButton, true); - } - } - - List songs = searchResult.getSongs(); - if (!songs.isEmpty()) - { - mergeAdapter.addView(songsHeading); - List displayedSongs = new ArrayList<>(songs.subList(0, Math.min(DEFAULT_SONGS, songs.size()))); - songAdapter = new EntryAdapter(getContext(), imageLoaderProvider.getValue().getImageLoader(), displayedSongs, false); - mergeAdapter.addAdapter(songAdapter); - if (songs.size() > DEFAULT_SONGS) - { - moreSongsAdapter = mergeAdapter.addView(moreSongsButton, true); - } - } - - boolean empty = searchResult.getArtists().isEmpty() && searchResult.getAlbums().isEmpty() && searchResult.getSongs().isEmpty(); - if (empty) mergeAdapter.addView(notFound, false); - } - - list.setAdapter(mergeAdapter); - } - - private void expandArtists() - { - artistAdapter.clear(); - - for (Artist artist : searchResult.getArtists()) - { - artistAdapter.add(artist); - } - - artistAdapter.notifyDataSetChanged(); - mergeAdapter.removeAdapter(moreArtistsAdapter); - mergeAdapter.notifyDataSetChanged(); - } - - private void expandAlbums() - { - albumAdapter.clear(); - - for (MusicDirectory.Entry album : searchResult.getAlbums()) - { - albumAdapter.add(album); - } - - albumAdapter.notifyDataSetChanged(); - mergeAdapter.removeAdapter(moreAlbumsAdapter); - mergeAdapter.notifyDataSetChanged(); - } - - private void expandSongs() - { - songAdapter.clear(); - - for (MusicDirectory.Entry song : searchResult.getSongs()) - { - songAdapter.add(song); - } - - songAdapter.notifyDataSetChanged(); - mergeAdapter.removeAdapter(moreSongsAdapter); - mergeAdapter.notifyDataSetChanged(); - } - - private void onArtistSelected(Artist artist) - { - Bundle bundle = new Bundle(); - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getId()); - Navigation.findNavController(getView()).navigate(R.id.searchToSelectAlbum, bundle); - } - - private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay) - { - Bundle bundle = new Bundle(); - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, album.getId()); - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, album.getTitle()); - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, album.isDirectory()); - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoplay); - Navigation.findNavController(getView()).navigate(R.id.searchToSelectAlbum, bundle); - } - - private void onSongSelected(MusicDirectory.Entry song, boolean append) - { - MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - if (mediaPlayerController != null) - { - if (!append) - { - mediaPlayerController.clear(); - } - - mediaPlayerController.addToPlaylist(Collections.singletonList(song), false, false, false, false, false); - - if (true) - { - mediaPlayerController.play(mediaPlayerController.getPlaylistSize() - 1); - } - - Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1)); - } - } - - private void onVideoSelected(MusicDirectory.Entry entry) - { - VideoPlayer.Companion.playVideo(getContext(), entry); - } - - private void autoplay() - { - if (!searchResult.getSongs().isEmpty()) - { - onSongSelected(searchResult.getSongs().get(0), false); - } - else if (!searchResult.getAlbums().isEmpty()) - { - onAlbumSelected(searchResult.getAlbums().get(0), true); - } - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt new file mode 100644 index 00000000..0bd32d29 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -0,0 +1,555 @@ +package org.moire.ultrasonic.fragment + +import android.app.SearchManager +import android.content.Context +import android.os.Bundle +import android.view.ContextMenu +import android.view.ContextMenu.ContextMenuInfo +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.widget.AdapterView.AdapterContextMenuInfo +import android.widget.ListAdapter +import android.widget.TextView +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.viewModels +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import androidx.navigation.Navigation +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.moire.ultrasonic.R +import org.moire.ultrasonic.adapters.ArtistRowBinder +import org.moire.ultrasonic.adapters.TrackViewBinder +import org.moire.ultrasonic.domain.Identifiable +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.SearchResult +import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle +import org.moire.ultrasonic.model.SearchListModel +import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker +import org.moire.ultrasonic.subsonic.ShareHandler +import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo +import org.moire.ultrasonic.util.CancellationToken +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.Settings +import org.moire.ultrasonic.util.Util.toast +import org.moire.ultrasonic.view.ArtistAdapter +import org.moire.ultrasonic.view.EntryAdapter +import timber.log.Timber + +/** + * Initiates a search on the media library and displays the results + */ +class SearchFragment : MultiListFragment(), KoinComponent { + private var artistsHeading: View? = null + private var albumsHeading: View? = null + private var songsHeading: View? = null + private var notFound: TextView? = null + private var moreArtistsButton: View? = null + private var moreAlbumsButton: View? = null + private var moreSongsButton: View? = null + private var searchResult: SearchResult? = null + private var artistAdapter: ArtistAdapter? = null + private var moreArtistsAdapter: ListAdapter? = null + private var moreAlbumsAdapter: ListAdapter? = null + private var moreSongsAdapter: ListAdapter? = null + private var searchRefresh: SwipeRefreshLayout? = null + + private val mediaPlayerController: MediaPlayerController by inject() + + private val shareHandler: ShareHandler by inject() + private val networkAndStorageChecker: NetworkAndStorageChecker by inject() + + private var cancellationToken: CancellationToken? = null + + override val listModel: SearchListModel by viewModels() + + override val recyclerViewId = R.id.search_list + + override val mainLayout: Int = R.layout.search + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + cancellationToken = CancellationToken() + setTitle(this, R.string.search_title) + setHasOptionsMenu(true) + + + val buttons = LayoutInflater.from(context).inflate(R.layout.search_buttons, + listView, false) + + if (buttons != null) { + artistsHeading = buttons.findViewById(R.id.search_artists) + albumsHeading = buttons.findViewById(R.id.search_albums) + songsHeading = buttons.findViewById(R.id.search_songs) + notFound = buttons.findViewById(R.id.search_not_found) + moreArtistsButton = buttons.findViewById(R.id.search_more_artists) + moreAlbumsButton = buttons.findViewById(R.id.search_more_albums) + moreSongsButton = buttons.findViewById(R.id.search_more_songs) + } + + + listModel.searchResult.observe(viewLifecycleOwner, { + if (it != null) populateList(it) + }) + + + searchRefresh = view.findViewById(R.id.search_entries_refresh) + searchRefresh!!.isEnabled = false + +// list.setOnItemClickListener(OnItemClickListener { parent: AdapterView<*>, view1: View, position: Int, id: Long -> +// if (view1 === moreArtistsButton) { +// expandArtists() +// } else if (view1 === moreAlbumsButton) { +// expandAlbums() +// } else if (view1 === moreSongsButton) { +// expandSongs() +// } else { +// val item = parent.getItemAtPosition(position) +// if (item is Artist) { +// onArtistSelected(item) +// } else if (item is MusicDirectory.Entry) { +// val entry = item +// if (entry.isDirectory) { +// onAlbumSelected(entry, false) +// } else if (entry.isVideo) { +// onVideoSelected(entry) +// } else { +// onSongSelected(entry, true) +// } +// } +// } +// }) + + registerForContextMenu(listView!!) + + + viewAdapter.register( + TrackViewBinder( + checkable = false, + draggable = false, + context = requireContext(), + lifecycleOwner = viewLifecycleOwner + ) + ) + + viewAdapter.register( + ArtistRowBinder( + { entry -> onItemClick(entry) }, + { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, + imageLoaderProvider.getImageLoader() + ) + ) + + + // Fragment was started with a query (e.g. from voice search), try to execute search right away + val arguments = arguments + if (arguments != null) { + val query = arguments.getString(Constants.INTENT_EXTRA_NAME_QUERY) + val autoPlay = arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) + if (query != null) { + return search(query, autoPlay) + } + } + + // Fragment was started from the Menu, create empty list + populateList(SearchResult()) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + val activity = activity ?: return + val searchManager = activity.getSystemService(Context.SEARCH_SERVICE) as SearchManager + inflater.inflate(R.menu.search, menu) + val searchItem = menu.findItem(R.id.search_item) + val searchView = searchItem.actionView as SearchView + searchView.setSearchableInfo(searchManager.getSearchableInfo(requireActivity().componentName)) + val arguments = arguments + val autoPlay = + arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) + val query = arguments?.getString(Constants.INTENT_EXTRA_NAME_QUERY) + // If started with a query, enter it to the searchView + if (query != null) { + searchView.setQuery(query, false) + searchView.clearFocus() + } + searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener { + override fun onSuggestionSelect(position: Int): Boolean { + return true + } + + override fun onSuggestionClick(position: Int): Boolean { + Timber.d("onSuggestionClick: %d", position) + val cursor = searchView.suggestionsAdapter.cursor + cursor.moveToPosition(position) + val suggestion = + cursor.getString(2) // TODO: Try to do something with this magic const -- 2 is the index of col containing suggestion name. + searchView.setQuery(suggestion, true) + return true + } + }) + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + Timber.d("onQueryTextSubmit: %s", query) + searchView.clearFocus() + search(query, autoPlay) + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + return true + } + }) + searchView.setIconifiedByDefault(false) + searchItem.expandActionView() + } + + // FIXME + override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) { + super.onCreateContextMenu(menu, view, menuInfo) + if (activity == null) return + val info = menuInfo as AdapterContextMenuInfo? +// val selectedItem = list!!.getItemAtPosition(info!!.position) +// val isArtist = selectedItem is Artist +// val isAlbum = selectedItem is MusicDirectory.Entry && selectedItem.isDirectory +// val inflater = requireActivity().menuInflater +// if (!isArtist && !isAlbum) { +// inflater.inflate(R.menu.select_song_context, menu) +// } else { +// inflater.inflate(R.menu.generic_context_menu, menu) +// } +// val shareButton = menu.findItem(R.id.menu_item_share) +// val downloadMenuItem = menu.findItem(R.id.menu_download) +// if (downloadMenuItem != null) { +// downloadMenuItem.isVisible = !isOffline() +// } +// if (isOffline() || isArtist) { +// if (shareButton != null) { +// shareButton.isVisible = false +// } +// } + } + + // FIXME + override fun onContextItemSelected(menuItem: MenuItem): Boolean { + val info = menuItem.menuInfo as AdapterContextMenuInfo +// val selectedItem = list!!.getItemAtPosition(info.position) +// val artist = if (selectedItem is Artist) selectedItem else null +// val entry = if (selectedItem is MusicDirectory.Entry) selectedItem else null +// var entryId: String? = null +// if (entry != null) { +// entryId = entry.id +// } +// val id = artist?.id ?: entryId ?: return true +// var songs: MutableList = ArrayList(1) +// val itemId = menuItem.itemId +// if (itemId == R.id.menu_play_now) { +// downloadHandler.downloadRecursively( +// this, +// id, +// false, +// false, +// true, +// false, +// false, +// false, +// false, +// false +// ) +// } else if (itemId == R.id.menu_play_next) { +// downloadHandler.downloadRecursively( +// this, +// id, +// false, +// true, +// false, +// true, +// false, +// true, +// false, +// false +// ) +// } else if (itemId == R.id.menu_play_last) { +// downloadHandler.downloadRecursively( +// this, +// id, +// false, +// true, +// false, +// false, +// false, +// false, +// false, +// false +// ) +// } else if (itemId == R.id.menu_pin) { +// downloadHandler.downloadRecursively( +// this, +// id, +// true, +// true, +// false, +// false, +// false, +// false, +// false, +// false +// ) +// } else if (itemId == R.id.menu_unpin) { +// downloadHandler.downloadRecursively( +// this, +// id, +// false, +// false, +// false, +// false, +// false, +// false, +// true, +// false +// ) +// } else if (itemId == R.id.menu_download) { +// downloadHandler.downloadRecursively( +// this, +// id, +// false, +// false, +// false, +// false, +// true, +// false, +// false, +// false +// ) +// } else if (itemId == R.id.song_menu_play_now) { +// if (entry != null) { +// songs = ArrayList(1) +// songs.add(entry) +// downloadHandler.download(this, false, false, true, false, false, songs) +// } +// } else if (itemId == R.id.song_menu_play_next) { +// if (entry != null) { +// songs = ArrayList(1) +// songs.add(entry) +// downloadHandler.download(this, true, false, false, true, false, songs) +// } +// } else if (itemId == R.id.song_menu_play_last) { +// if (entry != null) { +// songs = ArrayList(1) +// songs.add(entry) +// downloadHandler.download(this, true, false, false, false, false, songs) +// } +// } else if (itemId == R.id.song_menu_pin) { +// if (entry != null) { +// songs.add(entry) +// toast( +// context, +// resources.getQuantityString( +// R.plurals.select_album_n_songs_pinned, +// songs.size, +// songs.size +// ) +// ) +// downloadBackground(true, songs) +// } +// } else if (itemId == R.id.song_menu_download) { +// if (entry != null) { +// songs.add(entry) +// toast( +// context, +// resources.getQuantityString( +// R.plurals.select_album_n_songs_downloaded, +// songs.size, +// songs.size +// ) +// ) +// downloadBackground(false, songs) +// } +// } else if (itemId == R.id.song_menu_unpin) { +// if (entry != null) { +// songs.add(entry) +// toast( +// context, +// resources.getQuantityString( +// R.plurals.select_album_n_songs_unpinned, +// songs.size, +// songs.size +// ) +// ) +// mediaPlayerController.unpin(songs) +// } +// } else if (itemId == R.id.menu_item_share) { +// if (entry != null) { +// songs = ArrayList(1) +// songs.add(entry) +// shareHandler.createShare(this, songs, searchRefresh, cancellationToken!!) +// } +// return super.onContextItemSelected(menuItem) +// } else { +// return super.onContextItemSelected(menuItem) +// } + return true + } + + // OK! + override fun onDestroyView() { + cancellationToken?.cancel() + super.onDestroyView() + } + + // OK! + private fun downloadBackground(save: Boolean, songs: List) { + val onValid = Runnable { + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() + mediaPlayerController.downloadBackground(songs, save) + } + onValid.run() + } + + private fun search(query: String, autoplay: Boolean) { + // FIXME add error handler + // FIXME support autoplay + listModel.viewModelScope.launch { + listModel.search(query) + } + } + + private fun populateList(result: SearchResult) { + val searchResult = listModel.trimResultLength(result) + + val list = mutableListOf() + + val artists = searchResult.artists + if (artists.isNotEmpty()) { + // FIXME: addView(albumsHeading) + list.addAll(artists) + if (artists.size > DEFAULT_ARTISTS) { + // FIXME + //list.add((moreArtistsButton, true) + } + } + val albums = searchResult.albums + if (albums.isNotEmpty()) { + //mergeAdapter!!.addView(albumsHeading) + list.addAll(albums) + //mergeAdapter!!.addAdapter(albumAdapter) +// if (albums.size > DEFAULT_ALBUMS) { +// moreAlbumsAdapter = mergeAdapter!!.addView(moreAlbumsButton, true) +// } + } + val songs = searchResult.songs + if (songs.isNotEmpty()) { +// mergeAdapter!!.addView(songsHeading) + + list.addAll(songs) +// if (songs.size > DEFAULT_SONGS) { +// moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true) +// } + } + + // FIXME + if (list.isEmpty()) { + // mergeAdapter!!.addView(notFound, false) + } + + viewAdapter.submitList(list) + } + +// private fun expandArtists() { +// artistAdapter!!.clear() +// for (artist in searchResult!!.artists) { +// artistAdapter!!.add(artist) +// } +// artistAdapter!!.notifyDataSetChanged() +// mergeAdapter!!.removeAdapter(moreArtistsAdapter) +// mergeAdapter!!.notifyDataSetChanged() +// } +// +// private fun expandAlbums() { +// albumAdapter!!.clear() +// for (album in searchResult!!.albums) { +// albumAdapter!!.add(album) +// } +// albumAdapter!!.notifyDataSetChanged() +// mergeAdapter!!.removeAdapter(moreAlbumsAdapter) +// mergeAdapter!!.notifyDataSetChanged() +// } +// +// private fun expandSongs() { +// songAdapter!!.clear() +// for (song in searchResult!!.songs) { +// songAdapter!!.add(song) +// } +// songAdapter!!.notifyDataSetChanged() +// mergeAdapter!!.removeAdapter(moreSongsAdapter) +// mergeAdapter!!.notifyDataSetChanged() +// } +// +// private fun onArtistSelected(artist: Artist) { +// val bundle = Bundle() +// bundle.putString(Constants.INTENT_EXTRA_NAME_ID, artist.id) +// bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.id) +// Navigation.findNavController(requireView()).navigate(R.id.searchToSelectAlbum, bundle) +// } + + private fun onAlbumSelected(album: MusicDirectory.Entry, autoplay: Boolean) { + val bundle = Bundle() + bundle.putString(Constants.INTENT_EXTRA_NAME_ID, album.id) + bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, album.title) + bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, album.isDirectory) + bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoplay) + Navigation.findNavController(requireView()).navigate(R.id.searchToSelectAlbum, bundle) + } + + private fun onSongSelected(song: MusicDirectory.Entry, append: Boolean) { + if (!append) { + mediaPlayerController.clear() + } + mediaPlayerController.addToPlaylist(listOf(song), false, false, false, false, false) + mediaPlayerController.play(mediaPlayerController.playlistSize - 1) + toast(context, resources.getQuantityString(R.plurals.select_album_n_songs_added, 1, 1)) + } + + private fun onVideoSelected(entry: MusicDirectory.Entry) { + playVideo(requireContext(), entry) + } + + private fun autoplay() { + if (searchResult!!.songs.isNotEmpty()) { + onSongSelected(searchResult!!.songs[0], false) + } else if (searchResult!!.albums.isNotEmpty()) { + onAlbumSelected(searchResult!!.albums[0], true) + } + } + + companion object { + var DEFAULT_ARTISTS = Settings.defaultArtists + var DEFAULT_ALBUMS = Settings.defaultAlbums + var DEFAULT_SONGS = Settings.defaultSongs + } + + // FIXME!! + override fun getLiveData(args: Bundle?): LiveData> { + return MutableLiveData(listOf()) + } + + // FIXME + override val itemClickTarget: Int = 0 + + // FIXME + override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean { + return true + } + + // FIXME + override fun onItemClick(item: Identifiable) { + + } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt index f2a38aa9..c049d49c 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt @@ -8,8 +8,7 @@ import org.moire.ultrasonic.util.Util.getGrandparent class AlbumHeader( var entries: List, - var name: String, - songCount: Int + var name: String? ) : Identifiable { var isAllVideo: Boolean private set diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistView.java index f8919709..16c396ad 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistView.java @@ -20,7 +20,7 @@ package org.moire.ultrasonic.view; import android.content.Context; import android.view.LayoutInflater; -import android.widget.TextView; +import android.widget.LinearLayout; import org.moire.ultrasonic.R; import org.moire.ultrasonic.domain.Playlist; @@ -30,9 +30,9 @@ import org.moire.ultrasonic.domain.Playlist; * * @author Sindre Mehus */ -public class PlaylistView extends UpdateView +public class PlaylistView extends LinearLayout { - private Context context; + private final Context context; private PlaylistAdapter.ViewHolder viewHolder; public PlaylistView(Context context) @@ -45,7 +45,7 @@ public class PlaylistView extends UpdateView { LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true); viewHolder = new PlaylistAdapter.ViewHolder(); - viewHolder.name = (TextView) findViewById(R.id.playlist_name); + viewHolder.name = findViewById(R.id.playlist_name); setTag(viewHolder); } @@ -58,6 +58,5 @@ public class PlaylistView extends UpdateView public void setPlaylist(Playlist playlist) { viewHolder.name.setText(playlist.getName()); - update(); } } \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcatsChannelItemView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcastChannelItemView.java similarity index 85% rename from ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcatsChannelItemView.java rename to ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcastChannelItemView.java index 89163d86..367d01f4 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcatsChannelItemView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcastChannelItemView.java @@ -20,7 +20,7 @@ package org.moire.ultrasonic.view; import android.content.Context; import android.view.LayoutInflater; -import android.widget.TextView; +import android.widget.LinearLayout; import org.moire.ultrasonic.R; import org.moire.ultrasonic.domain.Playlist; @@ -30,12 +30,12 @@ import org.moire.ultrasonic.domain.Playlist; * * @author Sindre Mehus */ -public class PodcatsChannelItemView extends UpdateView +public class PodcastChannelItemView extends LinearLayout { - private Context context; + private final Context context; private PlaylistAdapter.ViewHolder viewHolder; - public PodcatsChannelItemView(Context context) + public PodcastChannelItemView(Context context) { super(context); this.context = context; @@ -45,7 +45,7 @@ public class PodcatsChannelItemView extends UpdateView { LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true); viewHolder = new PlaylistAdapter.ViewHolder(); - viewHolder.name = (TextView) findViewById(R.id.playlist_name); + viewHolder.name = findViewById(R.id.playlist_name); setTag(viewHolder); } @@ -58,6 +58,5 @@ public class PodcatsChannelItemView extends UpdateView public void setPlaylist(Playlist playlist) { viewHolder.name.setText(playlist.getName()); - update(); } } \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareView.java index ffe5fdce..0bed3b2c 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareView.java @@ -20,7 +20,7 @@ package org.moire.ultrasonic.view; import android.content.Context; import android.view.LayoutInflater; -import android.widget.TextView; +import android.widget.LinearLayout; import org.moire.ultrasonic.R; import org.moire.ultrasonic.domain.Share; @@ -30,9 +30,9 @@ import org.moire.ultrasonic.domain.Share; * * @author Joshua Bahnsen */ -public class ShareView extends UpdateView +public class ShareView extends LinearLayout { - private Context context; + private final Context context; private ShareAdapter.ViewHolder viewHolder; public ShareView(Context context) @@ -45,8 +45,8 @@ public class ShareView extends UpdateView { LayoutInflater.from(context).inflate(R.layout.share_list_item, this, true); viewHolder = new ShareAdapter.ViewHolder(); - viewHolder.url = (TextView) findViewById(R.id.share_url); - viewHolder.description = (TextView) findViewById(R.id.share_description); + viewHolder.url = findViewById(R.id.share_url); + viewHolder.description = findViewById(R.id.share_description); setTag(viewHolder); } @@ -60,6 +60,5 @@ public class ShareView extends UpdateView { viewHolder.url.setText(share.getName()); viewHolder.description.setText(share.getDescription()); - update(); } } \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index af38aaf4..7ec5c6ff 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -39,7 +39,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSettingDao import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.fragment.OnBackPressedHandler -import org.moire.ultrasonic.fragment.ServerSettingsModel +import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.provider.SearchSuggestionProvider import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.MediaPlayerController diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt similarity index 60% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowAdapter.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt index 61c79fea..6f2b7b2a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt @@ -1,5 +1,5 @@ /* - * AlbumRowAdapter.kt + * AlbumRowBinder.kt * Copyright (C) 2009-2021 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. @@ -9,13 +9,16 @@ package org.moire.ultrasonic.adapters import android.content.Context import android.graphics.drawable.Drawable +import android.view.LayoutInflater import android.view.MenuItem import android.view.View +import android.view.ViewGroup import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.recyclerview.widget.RecyclerView -import java.lang.Exception +import com.drakeet.multitype.ItemViewBinder +import org.koin.core.component.KoinComponent import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.imageloader.ImageLoader @@ -27,22 +30,12 @@ import timber.log.Timber /** * Creates a Row in a RecyclerView which contains the details of an Album */ -class AlbumRowAdapter( - itemList: List, - onItemClick: (MusicDirectory.Entry) -> Unit, - onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean, +class AlbumRowBinder( + val onItemClick: (MusicDirectory.Entry) -> Unit, + val onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean, private val imageLoader: ImageLoader, - onMusicFolderUpdate: (String?) -> Unit, context: Context, -) : GenericRowAdapter( - onItemClick, - onContextMenuClick, - onMusicFolderUpdate -) { - - init { - super.submitList(itemList) - } +) : ItemViewBinder(), KoinComponent { private val starDrawable: Drawable = Util.getDrawableFromAttribute(context, R.attr.star_full) @@ -50,34 +43,32 @@ class AlbumRowAdapter( Util.getDrawableFromAttribute(context, R.attr.star_hollow) // Set our layout files - override val layout = R.layout.album_list_item - override val contextMenuLayout = R.menu.artist_context_menu + val layout = R.layout.album_list_item + val contextMenuLayout = R.menu.artist_context_menu - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - if (holder is ViewHolder) { - val listPosition = if (selectFolderHeader != null) position - 1 else position - val entry = currentList[listPosition] - holder.album.text = entry.title - holder.artist.text = entry.artist - holder.details.setOnClickListener { onItemClick(entry) } - holder.details.setOnLongClickListener { view -> createPopupMenu(view, listPosition) } - holder.coverArtId = entry.coverArt - holder.star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable) - holder.star.setOnClickListener { onStarClick(entry, holder.star) } + override fun onBindViewHolder(holder: ViewHolder, item: MusicDirectory.Entry) { + holder.album.text = item.title + holder.artist.text = item.artist + holder.details.setOnClickListener { onItemClick(item) } + holder.details.setOnLongClickListener { + val popup = Helper.createPopupMenu(holder.itemView) - imageLoader.loadImage( - holder.coverArt, entry, - false, 0, R.drawable.unknown_album - ) + popup.setOnMenuItemClickListener { menuItem -> + onContextMenuClick(menuItem, item) + } + + true } + holder.coverArtId = item.coverArt + holder.star.setImageDrawable(if (item.starred) starDrawable else starHollowDrawable) + holder.star.setOnClickListener { onStarClick(item, holder.star) } + + imageLoader.loadImage( + holder.coverArt, item, + false, 0, R.drawable.unknown_album + ) } - override fun getItemCount(): Int { - if (selectFolderHeader != null) - return currentList.size + 1 - else - return currentList.size - } /** * Holds the view properties of an Item row @@ -93,12 +84,6 @@ class AlbumRowAdapter( var coverArtId: String? = null } - /** - * Creates an instance of our ViewHolder class - */ - override fun newViewHolder(view: View): RecyclerView.ViewHolder { - return ViewHolder(view) - } /** * Handles the star / unstar action for an album @@ -128,4 +113,9 @@ class AlbumRowAdapter( } }.start() } + + override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder { + return ViewHolder(inflater.inflate(layout, parent, false)) + } } + diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowAdapter.kt deleted file mode 100644 index 88837b68..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowAdapter.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * ArtistRowAdapter.kt - * Copyright (C) 2009-2021 Ultrasonic developers - * - * Distributed under terms of the GNU GPLv3 license. - */ - -package org.moire.ultrasonic.adapters - -import android.view.MenuItem -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter -import org.moire.ultrasonic.R -import org.moire.ultrasonic.domain.ArtistOrIndex -import org.moire.ultrasonic.imageloader.ImageLoader -import org.moire.ultrasonic.util.FileUtil -import org.moire.ultrasonic.util.Settings - -/** - * Creates a Row in a RecyclerView which contains the details of an Artist - */ -class ArtistRowAdapter( - itemList: List, - onItemClick: (ArtistOrIndex) -> Unit, - onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean, - private val imageLoader: ImageLoader, - onMusicFolderUpdate: (String?) -> Unit -) : GenericRowAdapter( - onItemClick, - onContextMenuClick, - onMusicFolderUpdate -), - SectionedAdapter { - - init { - super.submitList(itemList) - } - - // Set our layout files - override val layout = R.layout.artist_list_item - override val contextMenuLayout = R.menu.artist_context_menu - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - if (holder is ViewHolder) { - val listPosition = if (selectFolderHeader != null) position - 1 else position - holder.textView.text = currentList[listPosition].name - holder.section.text = getSectionForArtist(listPosition) - holder.layout.setOnClickListener { onItemClick(currentList[listPosition]) } - holder.layout.setOnLongClickListener { view -> createPopupMenu(view, listPosition) } - holder.coverArtId = currentList[listPosition].coverArt - - if (Settings.shouldShowArtistPicture) { - holder.coverArt.visibility = View.VISIBLE - val key = FileUtil.getArtistArtKey(currentList[listPosition].name, false) - imageLoader.loadImage( - view = holder.coverArt, - id = holder.coverArtId, - key = key, - large = false, - size = 0, - defaultResourceId = R.drawable.ic_contact_picture - ) - } else { - holder.coverArt.visibility = View.GONE - } - } - } - - override fun getSectionName(position: Int): String { - var listPosition = if (selectFolderHeader != null) position - 1 else position - - // Show the first artist's initial in the popup when the list is - // scrolled up to the "Select Folder" row - if (listPosition < 0) listPosition = 0 - - return getSectionFromName(currentList[listPosition].name ?: " ") - } - - private fun getSectionForArtist(artistPosition: Int): String { - if (artistPosition == 0) - return getSectionFromName(currentList[artistPosition].name ?: " ") - - val previousArtistSection = getSectionFromName( - currentList[artistPosition - 1].name ?: " " - ) - val currentArtistSection = getSectionFromName( - currentList[artistPosition].name ?: " " - ) - - return if (previousArtistSection == currentArtistSection) "" else currentArtistSection - } - - private fun getSectionFromName(name: String): String { - var section = name.first().uppercaseChar() - if (!section.isLetter()) section = '#' - return section.toString() - } - - /** - * Creates an instance of our ViewHolder class - */ - override fun newViewHolder(view: View): RecyclerView.ViewHolder { - return ViewHolder(view) - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt new file mode 100644 index 00000000..73aad3bf --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt @@ -0,0 +1,114 @@ +/* + * ArtistRowAdapter.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.adapters + +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.drakeet.multitype.ItemViewBinder +import org.koin.core.component.KoinComponent +import org.moire.ultrasonic.R +import org.moire.ultrasonic.domain.ArtistOrIndex +import org.moire.ultrasonic.imageloader.ImageLoader +import org.moire.ultrasonic.util.FileUtil +import org.moire.ultrasonic.util.Settings + +/** + * Creates a Row in a RecyclerView which contains the details of an Artist + * FIXME: On click wrong display... + */ +class ArtistRowBinder( + val onItemClick: (ArtistOrIndex) -> Unit, + val onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean, + private val imageLoader: ImageLoader, +): ItemViewBinder(), KoinComponent { + + val layout = R.layout.artist_list_item + val contextMenuLayout = R.menu.artist_context_menu + + override fun onBindViewHolder(holder: ViewHolder, item: ArtistOrIndex) { + holder.textView.text = item.name + holder.section.text = getSectionForArtist(item) + holder.layout.setOnClickListener { onItemClick(item) } + holder.layout.setOnLongClickListener { + val popup = Helper.createPopupMenu(holder.itemView) + + popup.setOnMenuItemClickListener { menuItem -> + onContextMenuClick(menuItem, item) + } + + true + } + + holder.coverArtId = item.coverArt + + if (Settings.shouldShowArtistPicture) { + holder.coverArt.visibility = View.VISIBLE + val key = FileUtil.getArtistArtKey(item.name, false) + imageLoader.loadImage( + view = holder.coverArt, + id = holder.coverArtId, + key = key, + large = false, + size = 0, + defaultResourceId = R.drawable.ic_contact_picture + ) + } else { + holder.coverArt.visibility = View.GONE + } + } + + private fun getSectionForArtist(item: ArtistOrIndex): String { + val index = adapter.items.indexOf(item) + + if (index == -1) return " " + + if (index == 0) return getSectionFromName(item.name ?: " ") + + val previousItem = adapter.items[index - 1] + val previousSectionKey: String + + if (previousItem is ArtistOrIndex) { + previousSectionKey = getSectionFromName(previousItem.name ?: " ") + } else { + previousSectionKey = " " + } + + val currentSectionKey = getSectionFromName(item.name ?: "") + + return if (previousSectionKey == currentSectionKey) "" else currentSectionKey + } + + private fun getSectionFromName(name: String): String { + var section = name.first().uppercaseChar() + if (!section.isLetter()) section = '#' + return section.toString() + } + + /** + * Creates an instance of our ViewHolder class + */ + class ViewHolder( + itemView: View + ) : RecyclerView.ViewHolder(itemView) { + var section: TextView = itemView.findViewById(R.id.row_section) + var textView: TextView = itemView.findViewById(R.id.row_artist_name) + var layout: RelativeLayout = itemView.findViewById(R.id.row_artist_layout) + var coverArt: ImageView = itemView.findViewById(R.id.artist_coverart) + var coverArtId: String? = null + } + + override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder { + return ViewHolder(inflater.inflate(layout, parent, false)) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt similarity index 97% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index 86929dd6..9cf73253 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MultiTypeDiffAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -11,7 +11,7 @@ import com.drakeet.multitype.MultiTypeAdapter import java.util.TreeSet import org.moire.ultrasonic.domain.Identifiable -class MultiTypeDiffAdapter : MultiTypeAdapter() { +class BaseAdapter : MultiTypeAdapter() { internal var selectedSet: TreeSet = TreeSet() internal var selectionRevision: MutableLiveData = MutableLiveData(0) @@ -43,7 +43,7 @@ class MultiTypeDiffAdapter : MultiTypeAdapter() { private val mListener = ListListener { previousList, currentList -> - this@MultiTypeDiffAdapter.onCurrentListChanged( + this@BaseAdapter.onCurrentListChanged( previousList, currentList ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt new file mode 100644 index 00000000..40e8290b --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt @@ -0,0 +1,127 @@ +package org.moire.ultrasonic.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.PopupMenu +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.drakeet.multitype.ItemViewBinder +import org.koin.core.component.KoinComponent +import org.moire.ultrasonic.R +import org.moire.ultrasonic.domain.Identifiable +import org.moire.ultrasonic.domain.MusicFolder +import org.moire.ultrasonic.service.RxBus +import java.lang.ref.WeakReference + +/** + * This little view shows the currently selected Folder (or catalog) on the music server. + * When clicked it will drop down a list of all available Folders and allow you to + * select one. The intended usage is to supply a filter to lists of artists, albums, etc + */ +class FolderSelectorBinder(context: Context +) : ItemViewBinder(), KoinComponent { + + private val weakContext: WeakReference = WeakReference(context) + + // 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), weakContext) + } + + override fun onBindViewHolder(holder: ViewHolder, item: FolderHeader) { + holder.setData(item.selected, item.folders) + } + + class ViewHolder( + view: View, + private val weakContext: WeakReference + ) : RecyclerView.ViewHolder(view) { + private var musicFolders: List = mutableListOf() + private var selectedFolderId: String? = null + private val folderName: TextView = itemView.findViewById(R.id.select_folder_name) + private val layout: LinearLayout = itemView.findViewById(R.id.select_folder_header) + + init { + folderName.text = weakContext.get()!!.getString(R.string.select_artist_all_folders) + layout.setOnClickListener { onFolderClick() } + } + + fun setData(selectedId: String?, folders: List) { + selectedFolderId = selectedId + musicFolders = folders + if (selectedFolderId != null) { + for ((id, name) in musicFolders) { + if (id == selectedFolderId) { + folderName.text = name + break + } + } + } else { + folderName.text = weakContext.get()!!.getString(R.string.select_artist_all_folders) + } + } + + private fun onFolderClick() { + val popup = PopupMenu(weakContext.get()!!, layout) + + var menuItem = popup.menu.add( + MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders + ) + if (selectedFolderId == null || selectedFolderId!!.isEmpty()) { + menuItem.isChecked = true + } + musicFolders.forEachIndexed { i, musicFolder -> + val (id, name) = musicFolder + menuItem = popup.menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, name) + if (id == selectedFolderId) { + menuItem.isChecked = true + } + } + + popup.menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true) + + popup.setOnMenuItemClickListener { item -> onFolderMenuItemSelected(item) } + popup.show() + } + + private fun onFolderMenuItemSelected(menuItem: MenuItem): Boolean { + val selectedFolder = if (menuItem.itemId == -1) null else musicFolders[menuItem.itemId] + val musicFolderName = selectedFolder?.name + ?: weakContext.get()!!.getString(R.string.select_artist_all_folders) + selectedFolderId = selectedFolder?.id + + menuItem.isChecked = true + folderName.text = musicFolderName + + RxBus.musicFolderChangedEventPublisher.onNext(selectedFolderId) + + return true + } + + companion object { + const val MENU_GROUP_MUSIC_FOLDER = 10 + } + } + + data class FolderHeader( + val folders: List, + val selected: String? + ): Identifiable { + override val id: String + get() = "FOLDERSELECTOR" + + override val longId: Long + get() = -1L + + override fun compareTo(other: Identifiable): Int { + return longId.compareTo(other.longId) + } + } + +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/GenericRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/GenericRowAdapter.kt deleted file mode 100644 index e95fb2c5..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/GenericRowAdapter.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * GenericRowAdapter.kt - * Copyright (C) 2009-2021 Ultrasonic developers - * - * Distributed under terms of the GNU GPLv3 license. - */ - -package org.moire.ultrasonic.adapters - -import android.annotation.SuppressLint -import android.view.LayoutInflater -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.PopupMenu -import android.widget.RelativeLayout -import android.widget.TextView -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import org.moire.ultrasonic.R -import org.moire.ultrasonic.data.ActiveServerProvider -import org.moire.ultrasonic.domain.Identifiable -import org.moire.ultrasonic.domain.MusicFolder -import org.moire.ultrasonic.view.SelectMusicFolderView - -/* -* An abstract Adapter, which can be extended to display a List of in a RecyclerView -*/ -abstract class GenericRowAdapter( - val onItemClick: (T) -> Unit, - val onContextMenuClick: (MenuItem, T) -> Boolean, - private val onMusicFolderUpdate: (String?) -> Unit -) : ListAdapter(GenericDiffCallback()) { - - protected abstract val layout: Int - protected abstract val contextMenuLayout: Int - - var folderHeaderEnabled: Boolean = true - var selectFolderHeader: SelectMusicFolderView? = null - var musicFolders: List = listOf() - var selectedFolder: String? = null - - /** - * Sets the content and state of the music folder selector row - */ - fun setFolderList(changedFolders: List, selectedId: String?) { - musicFolders = changedFolders - selectedFolder = selectedId - - selectFolderHeader?.setData( - selectedFolder, - musicFolders - ) - - notifyDataSetChanged() - } - - open fun newViewHolder(view: View): RecyclerView.ViewHolder { - return ViewHolder(view) - } - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): RecyclerView.ViewHolder { - if (viewType == TYPE_ITEM) { - val row = LayoutInflater.from(parent.context) - .inflate(layout, parent, false) - return newViewHolder(row) - } else { - val row = LayoutInflater.from(parent.context) - .inflate( - R.layout.select_folder_header, parent, false - ) - selectFolderHeader = SelectMusicFolderView(parent.context, row, onMusicFolderUpdate) - - if (musicFolders.isNotEmpty()) { - selectFolderHeader?.setData( - selectedFolder, - musicFolders - ) - } - - return selectFolderHeader!! - } - } - - abstract override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) - - override fun getItemCount(): Int { - if (selectFolderHeader != null) - return currentList.size + 1 - else - return currentList.size - } - - override fun getItemViewType(position: Int): Int { - return if (position == 0 && folderHeaderEnabled) TYPE_HEADER else TYPE_ITEM - } - - internal fun createPopupMenu(view: View, position: Int): Boolean { - val popup = PopupMenu(view.context, view) - val inflater: MenuInflater = popup.menuInflater - inflater.inflate(contextMenuLayout, popup.menu) - - val downloadMenuItem = popup.menu.findItem(R.id.menu_download) - downloadMenuItem?.isVisible = !ActiveServerProvider.isOffline() - - popup.setOnMenuItemClickListener { menuItem -> - onContextMenuClick(menuItem, currentList[position]) - } - popup.show() - return true - } - - /** - * Holds the view properties of an Item row - */ - class ViewHolder( - itemView: View - ) : RecyclerView.ViewHolder(itemView) { - var section: TextView = itemView.findViewById(R.id.row_section) - var textView: TextView = itemView.findViewById(R.id.row_artist_name) - var layout: RelativeLayout = itemView.findViewById(R.id.row_artist_layout) - var coverArt: ImageView = itemView.findViewById(R.id.artist_coverart) - var coverArtId: String? = null - } - - companion object { - internal const val TYPE_HEADER = 0 - internal const val TYPE_ITEM = 1 - - /** - * Calculates the differences between data sets - */ - class GenericDiffCallback : DiffUtil.ItemCallback() { - @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 - } - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt index 9a82885d..33826e48 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.drakeet.multitype.ItemViewBinder import java.lang.ref.WeakReference @@ -57,7 +58,12 @@ class HeaderViewBinder( Util.getAlbumImageSize(context) ) - holder.titleView.text = item.name + if (item.name != null) { + holder.titleView.isVisible = true + holder.titleView.text = item.name + } else { + holder.titleView.isVisible = false + } // Don't show a header if all entries are videos if (item.isAllVideo) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt new file mode 100644 index 00000000..ca510db5 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt @@ -0,0 +1,22 @@ +package org.moire.ultrasonic.adapters + +import android.view.MenuInflater +import android.view.View +import android.widget.PopupMenu +import org.moire.ultrasonic.R +import org.moire.ultrasonic.data.ActiveServerProvider + +object Helper { + @JvmStatic + fun createPopupMenu(view: View, contextMenuLayout: Int = R.menu.artist_context_menu): PopupMenu { + val popup = PopupMenu(view.context, view) + val inflater: MenuInflater = popup.menuInflater + inflater.inflate(contextMenuLayout, popup.menu) + + val downloadMenuItem = popup.menu.findItem(R.id.menu_download) + downloadMenuItem?.isVisible = !ActiveServerProvider.isOffline() + + popup.show() + return popup + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/SectionedAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/SectionedAdapter.kt new file mode 100644 index 00000000..aabd48f1 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/SectionedAdapter.kt @@ -0,0 +1,18 @@ +package org.moire.ultrasonic.adapters + +import com.drakeet.multitype.MultiTypeAdapter +import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView +import org.moire.ultrasonic.domain.Identifiable + +class SectionedAdapter : MultiTypeAdapter(), FastScrollRecyclerView.SectionedAdapter { + override fun getSectionName(position: Int): String { +// var listPosition = if (selectFolderHeader != null) position - 1 else position +// +// // Show the first artist's initial in the popup when the list is +// // scrolled up to the "Select Folder" row +// if (listPosition < 0) listPosition = 0 +// +// return getSectionFromName(currentList[listPosition].name ?: " ") + return "X" + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt index 2b9e4be1..89c7a5ea 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt @@ -17,6 +17,7 @@ import androidx.core.content.ContextCompat import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting +import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.util.ServerColor import org.moire.ultrasonic.util.Util diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index 5fdd494b..537847b5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -22,16 +22,6 @@ class TrackViewBinder( private val onClickCallback: ((View, DownloadFile?) -> Unit)? = null ) : ItemViewBinder(), 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 @@ -44,9 +34,8 @@ class TrackViewBinder( } override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) { - val downloadFile: DownloadFile? - val _adapter = adapter as MultiTypeDiffAdapter<*> + val diffAdapter = adapter as BaseAdapter<*> when (item) { is MusicDirectory.Entry -> { @@ -66,7 +55,7 @@ class TrackViewBinder( file = downloadFile, checkable = checkable, draggable = draggable, - _adapter.isSelected(item.longId) + diffAdapter.isSelected(item.longId) ) // Notify the adapter of selection changes @@ -74,18 +63,18 @@ class TrackViewBinder( lifecycleOwner, { newValue -> if (newValue) { - _adapter.notifySelected(item.longId) + diffAdapter.notifySelected(item.longId) } else { - _adapter.notifyUnselected(item.longId) + diffAdapter.notifyUnselected(item.longId) } } ) // Listen to changes in selection status and update ourselves - _adapter.selectionRevision.observe( + diffAdapter.selectionRevision.observe( lifecycleOwner, { - val newStatus = _adapter.isSelected(item.longId) + val newStatus = diffAdapter.isSelected(item.longId) if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus } @@ -96,7 +85,7 @@ class TrackViewBinder( lifecycleOwner, { holder.updateStatus(it) - _adapter.notifyChanged() + diffAdapter.notifyChanged() } ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 68a5c24f..e861836a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -23,13 +23,14 @@ import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.DownloadStatus import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.service.RxBus 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 + * FIXME: Add video List item */ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable, KoinComponent { @@ -58,7 +59,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable private var isMaximized = false private var cachedStatus = DownloadStatus.UNKNOWN private var statusImage: Drawable? = null - private var playing = false + private var isPlayingCached = false var observableChecked = MutableLiveData(false) @@ -67,8 +68,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable features.isFeatureEnabled(Feature.FIVE_STAR_RATING) } - private val mediaPlayerController: MediaPlayerController by inject() - lateinit var imageHelper: ImageHelper init { @@ -116,9 +115,44 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable setupStarButtons(song) } - update() + updateProgress(downloadFile!!.progress.value!!) + updateStatus(downloadFile!!.status.value!!) + + if (useFiveStarRating) { + setFiveStars(entry?.userRating ?: 0) + } else { + setSingleStar(entry!!.starred) + } + + RxBus.playerStateObservable.subscribe { + setPlayIcon(it.track == downloadFile) + } + + // Minimize or maximize the Text view (if song title is very long) + itemView.setOnLongClickListener { + if (!song.isDirectory) { + maximizeOrMinimize() + true + } + false + } } + private fun setPlayIcon(isPlaying: Boolean) { + if (isPlaying && !isPlayingCached) { + isPlayingCached = true + title.setCompoundDrawablesWithIntrinsicBounds( + imageHelper.playingImage, null, null, null + ) + } else if (!isPlaying && isPlayingCached) { + isPlayingCached = false + title.setCompoundDrawablesWithIntrinsicBounds( + 0, 0, 0, 0 + ) + } + } + + private fun setupStarButtons(song: MusicDirectory.Entry) { if (useFiveStarRating) { // Hide single star @@ -157,38 +191,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable } } - @Synchronized - // TODO: Should be removed - fun update() { - - updateProgress(downloadFile!!.progress.value!!) - updateStatus(downloadFile!!.status.value!!) - - 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) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt index 992141e1..79209e5b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt @@ -9,7 +9,7 @@ import org.moire.ultrasonic.data.AppDatabase import org.moire.ultrasonic.data.MIGRATION_1_2 import org.moire.ultrasonic.data.MIGRATION_2_3 import org.moire.ultrasonic.data.MIGRATION_3_4 -import org.moire.ultrasonic.fragment.ServerSettingsModel +import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.util.Settings const val SP_NAME = "Default_SP" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 559c721e..d2d29d49 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -7,12 +7,14 @@ import androidx.lifecycle.LiveData import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.RecyclerView import org.moire.ultrasonic.R +import org.moire.ultrasonic.adapters.AlbumRowBinder import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.model.AlbumListModel import org.moire.ultrasonic.util.Constants /** * Displays a list of Albums from the media library - * TODO: Check refresh is working + * FIXME: Add music folder support */ class AlbumListFragment : EntryListFragment() { @@ -54,24 +56,6 @@ class AlbumListFragment : EntryListFragment() { return listModel.getAlbumList(refresh or append, refreshListView!!, args) } -// FIXME -// /** -// * Provide the Adapter for the RecyclerView with a lazy delegate -// */ -// override val viewAdapter: AlbumRowAdapter by lazy { -// AlbumRowAdapter( -// liveDataItems.value ?: listOf(), -// { entry -> onItemClick(entry) }, -// { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, -// imageLoaderProvider.getImageLoader(), -// onMusicFolderUpdate, -// requireContext() -// ) -// } - - val newBundleClone: Bundle - get() = arguments?.clone() as Bundle - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -81,13 +65,25 @@ class AlbumListFragment : EntryListFragment() { override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) { // Triggered only when new data needs to be appended to the list // Add whatever code is needed to append new items to the bottom of the list - val appendArgs = newBundleClone + val appendArgs = getArgumentsClone() appendArgs.putBoolean(Constants.INTENT_EXTRA_NAME_APPEND, true) getLiveData(appendArgs) } } addOnScrollListener(scrollListener) } + + + viewAdapter.register( + AlbumRowBinder( + { entry -> onItemClick(entry) }, + { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, + imageLoaderProvider.getImageLoader(), + context = requireContext() + ) + ) + + } override fun onItemClick(item: MusicDirectory.Entry) { @@ -98,4 +94,5 @@ class AlbumListFragment : EntryListFragment() { bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent) findNavController().navigate(itemClickTarget, bundle) } + } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index d0c30d59..2b092a46 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -1,12 +1,17 @@ package org.moire.ultrasonic.fragment import android.os.Bundle +import android.view.View import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData +import androidx.navigation.fragment.findNavController import org.moire.ultrasonic.R -import org.moire.ultrasonic.adapters.ArtistRowAdapter +import org.moire.ultrasonic.adapters.ArtistRowBinder +import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.ArtistOrIndex +import org.moire.ultrasonic.model.ArtistListModel import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.Settings /** * Displays the list of Artists from the media library @@ -39,6 +44,7 @@ class ArtistListFragment : EntryListFragment() { */ override val itemClickTarget = R.id.selectArtistToSelectAlbum + /** * The central function to pass a query to the model and return a LiveData object */ @@ -47,17 +53,31 @@ class ArtistListFragment : EntryListFragment() { return listModel.getItems(refresh, refreshListView!!) } - /** - * Provide the Adapter for the RecyclerView with a lazy delegate - */ - // FIXME -// override val viewAdapter: ArtistRowAdapter by lazy { -// ArtistRowAdapter( -// liveDataItems.value ?: listOf(), -// { entry -> onItemClick(entry) }, -// { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, -// imageLoaderProvider.getImageLoader(), -// onMusicFolderUpdate -// ) -// } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewAdapter.register( + ArtistRowBinder( + { entry -> onItemClick(entry) }, + { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, + imageLoaderProvider.getImageLoader() + ) + ) + } + + override fun onItemClick(item: ArtistOrIndex) { + 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)) + bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, Constants.ALPHABETICAL_BY_NAME) + bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) + bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) + bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) + findNavController().navigate(itemClickTarget, bundle) + } + + //Constants.ALPHABETICAL_BY_NAME + } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt new file mode 100644 index 00000000..f0e2e4bd --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -0,0 +1,66 @@ +package org.moire.ultrasonic.fragment + +import android.os.Bundle +import android.view.View +import androidx.core.view.isVisible +import androidx.lifecycle.LiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import org.moire.ultrasonic.R +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle + +/** + * Lists the Bookmarks available on the server + */ +class BookmarksFragment : TrackCollectionFragment() { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setTitle(this, R.string.button_bar_bookmarks) + } + + override fun setupButtons(view: View) { + super.setupButtons(view) + + // Why? + selectButton?.visibility = View.GONE + playNextButton?.visibility = View.GONE + playLastButton?.visibility = View.GONE + moreButton?.visibility = View.GONE + } + + override fun getLiveData(args: Bundle?): LiveData> { + listModel.viewModelScope.launch(handler) { + refreshListView?.isRefreshing = true + listModel.getBookmarks() + refreshListView?.isRefreshing = false + } + return listModel.currentList + } + + override fun enableButtons(selection: List) { + val enabled = selection.isNotEmpty() + var unpinEnabled = false + var deleteEnabled = false + var pinnedCount = 0 + + for (song in selection) { + val downloadFile = mediaPlayerController.getDownloadFileForSong(song) + if (downloadFile.isWorkDone) { + deleteEnabled = true + } + if (downloadFile.isSaved) { + pinnedCount++ + unpinEnabled = true + } + } + + playNowButton?.isVisible = (enabled && deleteEnabled) + pinButton?.isVisible = (enabled && !isOffline() && selection.size > pinnedCount) + unpinButton!!.isVisible = (enabled && unpinEnabled) + downloadButton!!.isVisible = (enabled && !deleteEnabled && !isOffline()) + deleteButton!!.isVisible = (enabled && deleteEnabled) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index 6acaafff..86dc63af 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.LiveData import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.TrackViewBinder +import org.moire.ultrasonic.model.GenericListModel import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.Downloader import org.moire.ultrasonic.util.Util diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt index 1addcf40..48d75a62 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -33,6 +33,7 @@ import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse import org.moire.ultrasonic.api.subsonic.throwOnFailure import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting +import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.ErrorDialog diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt new file mode 100644 index 00000000..5cef4c8f --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -0,0 +1,140 @@ +package org.moire.ultrasonic.fragment + +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import androidx.navigation.fragment.findNavController +import org.moire.ultrasonic.R +import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.GenericEntry +import org.moire.ultrasonic.service.RxBus +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.Settings + +/** + * An extension of the MultiListFragment, with a few helper functions geared + * towards the display of MusicDirectory.Entries. + * @param T: The type of data which will be used (must extend GenericEntry) + */ +abstract class EntryListFragment : MultiListFragment() { + + /** + * Whether to show the folder selector + */ + // FIXME + fun showFolderHeader(): Boolean { + return listModel.showSelectFolderHeader(arguments) && + !listModel.isOffline() && !Settings.shouldUseId3Tags + } + + @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) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // FIXME: What to do when the user has modified the folder filter + RxBus.musicFolderChangedEventObservable.subscribe { + if (!listModel.isOffline()) { + val currentSetting = listModel.activeServer + currentSetting.musicFolderId = it + serverSettingsModel.updateItem(currentSetting) + } + viewAdapter.notifyDataSetChanged() + listModel.refresh(refreshListView!!, arguments) + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt deleted file mode 100644 index befd90e8..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt +++ /dev/null @@ -1,281 +0,0 @@ -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 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 -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 GenericListFragment> : 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> - - /** - * The central function to pass a query to the model and return a LiveData object - */ - abstract fun getLiveData(args: Bundle? = null): LiveData> - - /** - * 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 -> - 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(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 : MultiListFragment() { - @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) - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index 49cf0e56..22748d89 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -8,20 +8,21 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView 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.MultiTypeDiffAdapter +import org.moire.ultrasonic.adapters.BaseAdapter import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Identifiable -import org.moire.ultrasonic.domain.MusicFolder +import org.moire.ultrasonic.model.GenericListModel +import org.moire.ultrasonic.model.ServerSettingsModel 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 @@ -43,8 +44,8 @@ abstract class MultiListFragment : Fragment() { * The Adapter for the RecyclerView * Recommendation: Implement this as a lazy delegate */ - internal val viewAdapter: MultiTypeDiffAdapter by lazy { - MultiTypeDiffAdapter() + internal val viewAdapter: BaseAdapter by lazy { + BaseAdapter() } /** @@ -61,7 +62,9 @@ abstract class MultiListFragment : Fragment() { /** * The central function to pass a query to the model and return a LiveData object */ - abstract fun getLiveData(args: Bundle? = null): LiveData> + open fun getLiveData(args: Bundle? = null): LiveData> { + return MutableLiveData(listOf()) + } /** * The id of the target in the navigation graph where we should go, @@ -84,35 +87,6 @@ abstract class MultiListFragment : Fragment() { */ open val recyclerViewId = R.id.generic_list_recycler - /** - * The observer to be called if the available music folders have changed - */ - @Suppress("CommentOverPrivateProperty") - private val musicFolderObserver = { folders: List -> - // 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( @@ -150,9 +124,6 @@ abstract class MultiListFragment : Fragment() { } ) - // Setup the Music folder handling - listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver) - // Create a View Manager viewManager = LinearLayoutManager(this.context) @@ -184,103 +155,17 @@ abstract class MultiListFragment : Fragment() { abstract fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean abstract fun onItemClick(item: T) + + fun getArgumentsClone(): Bundle { + var bundle: Bundle + + try { + bundle = arguments?.clone() as Bundle + } catch (ignored: Exception) { + bundle = Bundle() + } + + return bundle + } } -// abstract class EntryListFragment> : -// GenericListFragment() { -// @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) -// } -// } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index e6a9eb21..3ca32373 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -60,7 +60,7 @@ import org.koin.android.ext.android.inject import org.koin.core.component.KoinComponent import org.koin.core.component.get import org.moire.ultrasonic.R -import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter +import org.moire.ultrasonic.adapters.BaseAdapter import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.audiofx.VisualizerController @@ -154,8 +154,8 @@ class PlayerFragment : private lateinit var fullStar: Drawable private lateinit var progressBar: SeekBar - internal val viewAdapter: MultiTypeDiffAdapter by lazy { - MultiTypeDiffAdapter() + internal val viewAdapter: BaseAdapter by lazy { + BaseAdapter() } override fun onCreate(savedInstanceState: Bundle?) { @@ -890,7 +890,7 @@ class PlayerFragment : // FIXME: // Needs to be changed in the playlist as well... // Move it in the data set - (recyclerView.adapter as MultiTypeDiffAdapter<*>).moveItem(from, to) + (recyclerView.adapter as BaseAdapter<*>).moveItem(from, to) return true } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt index 05d7b568..9e1eeabe 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt @@ -18,6 +18,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.fragment.EditServerFragment.Companion.EDIT_SERVER_INTENT_INDEX +import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.util.Util import timber.log.Timber diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index fb86404f..18f8f577 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -25,7 +25,6 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.Navigation import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import java.util.Collections import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.launch import org.koin.android.ext.android.inject @@ -36,9 +35,11 @@ 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.setTitle +import org.moire.ultrasonic.model.TrackCollectionModel import org.moire.ultrasonic.service.MediaPlayerController 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 @@ -47,35 +48,34 @@ import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util import timber.log.Timber +import java.util.Collections /** * Displays a group of tracks, eg. the songs of an album, of a playlist etc. - * TODO: Move Clickhandler into ViewBinders - * TODO: Fix clikc handlers and context menus etc. + * FIXME: Offset when navigating to? */ -class TrackCollectionFragment : - MultiListFragment() { +open class TrackCollectionFragment : MultiListFragment() { private var albumButtons: View? = null private var emptyView: TextView? = null - private var selectButton: ImageView? = null - private var playNowButton: ImageView? = null - private var playNextButton: ImageView? = null - private var playLastButton: ImageView? = null - private var pinButton: ImageView? = null - private var unpinButton: ImageView? = null - private var downloadButton: ImageView? = null - private var deleteButton: ImageView? = null - private var moreButton: ImageView? = null + internal var selectButton: ImageView? = null + internal var playNowButton: ImageView? = null + internal var playNextButton: ImageView? = null + internal var playLastButton: ImageView? = null + internal var pinButton: ImageView? = null + internal var unpinButton: ImageView? = null + internal var downloadButton: ImageView? = null + internal var deleteButton: ImageView? = null + internal var moreButton: ImageView? = null private var playAllButtonVisible = false private var shareButtonVisible = false private var playAllButton: MenuItem? = null private var shareButton: MenuItem? = null - private val mediaPlayerController: MediaPlayerController by inject() + internal val mediaPlayerController: MediaPlayerController by inject() private val networkAndStorageChecker: NetworkAndStorageChecker by inject() private val shareHandler: ShareHandler by inject() - private var cancellationToken: CancellationToken? = null + internal var cancellationToken: CancellationToken? = null override val listModel: TrackCollectionModel by viewModels() @@ -98,7 +98,6 @@ class TrackCollectionFragment : * 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 onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -110,90 +109,15 @@ class TrackCollectionFragment : // Setup refresh handler refreshListView = view.findViewById(refreshListId) refreshListView?.setOnRefreshListener { - updateDisplay(true) + refreshData(true) } listModel.currentList.observe(viewLifecycleOwner, updateInterfaceWithEntries) listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver) -// 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 -// } + setupButtons(view) - selectButton = view.findViewById(R.id.select_album_select) - playNowButton = view.findViewById(R.id.select_album_play_now) - playNextButton = view.findViewById(R.id.select_album_play_next) - playLastButton = view.findViewById(R.id.select_album_play_last) - pinButton = view.findViewById(R.id.select_album_pin) - unpinButton = view.findViewById(R.id.select_album_unpin) - downloadButton = view.findViewById(R.id.select_album_download) - deleteButton = view.findViewById(R.id.select_album_delete) - moreButton = view.findViewById(R.id.select_album_more) - emptyView = TextView(requireContext()) - - selectButton!!.setOnClickListener { - selectAllOrNone() - } - - playNowButton!!.setOnClickListener { - playNow(false) - } - - playNextButton!!.setOnClickListener { - downloadHandler.download( - this@TrackCollectionFragment, append = true, - save = false, autoPlay = false, playNext = true, shuffle = false, - songs = getSelectedSongs() - ) - } - - playLastButton!!.setOnClickListener { - playNow(true) - } - - pinButton!!.setOnClickListener { - downloadBackground(true) - } - - unpinButton!!.setOnClickListener { - unpin() - } - - downloadButton!!.setOnClickListener { - downloadBackground(false) - } - - deleteButton!!.setOnClickListener { - delete() - } + emptyView = view.findViewById(R.id.select_album_empty) registerForContextMenu(listView!!) setHasOptionsMenu(true) @@ -234,19 +158,68 @@ class TrackCollectionFragment : ) // Loads the data - updateDisplay(false) + refreshData(false) + } + + internal open fun setupButtons(view: View) { + selectButton = view.findViewById(R.id.select_album_select) + playNowButton = view.findViewById(R.id.select_album_play_now) + playNextButton = view.findViewById(R.id.select_album_play_next) + playLastButton = view.findViewById(R.id.select_album_play_last) + pinButton = view.findViewById(R.id.select_album_pin) + unpinButton = view.findViewById(R.id.select_album_unpin) + downloadButton = view.findViewById(R.id.select_album_download) + deleteButton = view.findViewById(R.id.select_album_delete) + moreButton = view.findViewById(R.id.select_album_more) + + selectButton?.setOnClickListener { + selectAllOrNone() + } + + playNowButton?.setOnClickListener { + playNow(false) + } + + playNextButton?.setOnClickListener { + downloadHandler.download( + this@TrackCollectionFragment, append = true, + save = false, autoPlay = false, playNext = true, shuffle = false, + songs = getSelectedSongs() + ) + } + + playLastButton!!.setOnClickListener { + playNow(true) + } + + pinButton?.setOnClickListener { + downloadBackground(true) + } + + unpinButton?.setOnClickListener { + unpin() + } + + downloadButton?.setOnClickListener { + downloadBackground(false) + } + + deleteButton?.setOnClickListener { + delete() + } } val handler = CoroutineExceptionHandler { _, exception -> Handler(Looper.getMainLooper()).post { CommunicationError.handleError(exception, context) } - refreshListView!!.isRefreshing = false + refreshListView?.isRefreshing = false } - private fun updateDisplay(refresh: Boolean) { - // FIXME: Use refresh - getLiveData(requireArguments()) + private fun refreshData(refresh: Boolean = false) { + val args = getArgumentsClone() + args.putBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, refresh) + getLiveData(args) } override fun onContextItemSelected(menuItem: MenuItem): Boolean { @@ -370,7 +343,6 @@ class TrackCollectionFragment : this, append, false, !append, playNext = false, shuffle = false, songs = selectedSongs ) - selectAll(selected = false, toast = false) } else { playAll(false, append) } @@ -399,8 +371,10 @@ class TrackCollectionFragment : } } - val isArtist = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false) - val id = requireArguments().getString(Constants.INTENT_EXTRA_NAME_ID) + val isArtist = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false)?: false + + // FIXME WHICH id if no arguments? + val id = arguments?.getString(Constants.INTENT_EXTRA_NAME_ID) if (hasSubFolders && id != null) { downloadHandler.downloadRecursively( @@ -435,13 +409,13 @@ class TrackCollectionFragment : } as List } - private fun selectAllOrNone() { + internal fun selectAllOrNone() { val someUnselected = viewAdapter.selectedSet.size < childCount selectAll(someUnselected, true) } - private fun selectAll(selected: Boolean, toast: Boolean) { + internal fun selectAll(selected: Boolean, toast: Boolean) { var selectedCount = viewAdapter.selectedSet.size * -1 selectedCount += viewAdapter.setSelectionStatusOfAll(selected) @@ -453,7 +427,7 @@ class TrackCollectionFragment : } } - private fun enableButtons(selection: List = getSelectedSongs()) { + internal open fun enableButtons(selection: List = getSelectedSongs()) { val enabled = selection.isNotEmpty() var unpinEnabled = false var deleteEnabled = false @@ -480,7 +454,7 @@ class TrackCollectionFragment : deleteButton?.isVisible = (enabled && deleteEnabled) } - private fun downloadBackground(save: Boolean) { + internal fun downloadBackground(save: Boolean) { var songs = getSelectedSongs() if (songs.isEmpty()) { @@ -514,7 +488,7 @@ class TrackCollectionFragment : onValid.run() } - private fun delete() { + internal fun delete() { val songs = getSelectedSongs() Util.toast( @@ -527,7 +501,7 @@ class TrackCollectionFragment : mediaPlayerController.delete(songs) } - private fun unpin() { + internal fun unpin() { val songs = getSelectedSongs() Util.toast( context, @@ -586,23 +560,17 @@ class TrackCollectionFragment : } } - val listSize = requireArguments().getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) + val listSize = arguments?.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) ?: 0 + + // Hide select button for video lists + selectButton!!.isVisible = !allVideos if (songCount > 0) { - pinButton!!.visibility = View.VISIBLE - unpinButton!!.visibility = View.VISIBLE - downloadButton!!.visibility = View.VISIBLE - deleteButton!!.visibility = View.VISIBLE - selectButton!!.visibility = if (allVideos) View.GONE else View.VISIBLE - playNowButton!!.visibility = View.VISIBLE - playNextButton!!.visibility = View.VISIBLE - playLastButton!!.visibility = View.VISIBLE - if (listSize == 0 || songCount < listSize) { moreButton!!.visibility = View.GONE } else { moreButton!!.visibility = View.VISIBLE - if (requireArguments().getInt(Constants.INTENT_EXTRA_NAME_RANDOM, 0) > 0) { + if (arguments?.getInt(Constants.INTENT_EXTRA_NAME_RANDOM, 0) ?:0 > 0) { moreButton!!.setOnClickListener { val offset = requireArguments().getInt( Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0 @@ -617,58 +585,41 @@ class TrackCollectionFragment : } } } - } else { - - // TODO: This code path can be removed when getArtist has been moved to - // AlbumListFragment (getArtist returns the albums of an artist) - pinButton!!.visibility = View.GONE - unpinButton!!.visibility = View.GONE - downloadButton!!.visibility = View.GONE - deleteButton!!.visibility = View.GONE - selectButton!!.visibility = View.GONE - playNowButton!!.visibility = View.GONE - playNextButton!!.visibility = View.GONE - playLastButton!!.visibility = View.GONE - - if (listSize == 0 || entryList.size < listSize) { - albumButtons!!.visibility = View.GONE - } else { - moreButton!!.visibility = View.VISIBLE - } } + // Show a text if we have no entries + emptyView?.isVisible = entryList.isEmpty() + enableButtons() - val isAlbumList = requireArguments().containsKey( + val isAlbumList = arguments?.containsKey( Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE - ) + )?:false playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos shareButtonVisible = !isOffline() && songCount > 0 - if (playAllButton != null) { - playAllButton!!.isVisible = playAllButtonVisible - } - - if (shareButton != null) { - shareButton!!.isVisible = shareButtonVisible - } + playAllButton?.isVisible = playAllButtonVisible + shareButton?.isVisible = shareButtonVisible if (songCount > 0 && listModel.showHeader) { val name = listModel.currentDirectory.value?.name - val intentAlbumName = requireArguments().getString(Constants.INTENT_EXTRA_NAME_NAME, "Name")!! - val albumHeader = AlbumHeader(it, name ?: intentAlbumName, songCount) + val intentAlbumName = arguments?.getString(Constants.INTENT_EXTRA_NAME_NAME, "") + val albumHeader = AlbumHeader(it, name ?: intentAlbumName) val mixedList: MutableList = mutableListOf(albumHeader) mixedList.addAll(entryList) + Timber.e("SUBMITTING MIXED LIST") viewAdapter.submitList(mixedList) } else { + Timber.e("SUBMITTING ENTRY LIST") viewAdapter.submitList(entryList) } - val playAll = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) + val playAll = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)?:false + if (playAll && songCount > 0) { playAll( - requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false), + arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)?:false, false ) } @@ -722,9 +673,7 @@ class TrackCollectionFragment : val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true) listModel.viewModelScope.launch(handler) { - refreshListView!!.isRefreshing = true - - listModel.getMusicFolders(refresh) + refreshListView?.isRefreshing = true if (playlistId != null) { setTitle(playlistName!!) @@ -753,14 +702,14 @@ class TrackCollectionFragment : if (isAlbum) { listModel.getAlbum(refresh, id!!, name, parentId) } else { - listModel.getArtist(refresh, id!!, name) + throw IllegalAccessException("Use AlbumFragment instead!") } } else { listModel.getMusicDirectory(refresh, id!!, name, parentId) } } - refreshListView!!.isRefreshing = false + refreshListView?.isRefreshing = false } return listModel.currentList } @@ -774,6 +723,24 @@ class TrackCollectionFragment : } override fun onItemClick(item: MusicDirectory.Entry) { - // nothing + when { + item.isDirectory -> { + val bundle = Bundle() + bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id) + bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, item.isDirectory) + bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.title) + bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent) + Navigation.findNavController(requireView()).navigate( + R.id.trackCollectionFragment, + bundle + ) + } + item.isVideo -> { + VideoPlayer.playVideo(requireContext(), item) + } + else -> { + enableButtons() + } + } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt similarity index 66% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt index a9216173..a969afbd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -1,10 +1,11 @@ -package org.moire.ultrasonic.fragment +package org.moire.ultrasonic.model import android.app.Application import android.os.Bundle import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.service.MusicService @@ -13,7 +14,9 @@ import org.moire.ultrasonic.util.Settings class AlbumListModel(application: Application) : GenericListModel(application) { - val albumList: MutableLiveData> = MutableLiveData(listOf()) + + + val list: MutableLiveData> = MutableLiveData(listOf()) var lastType: String? = null private var loadedUntil: Int = 0 @@ -26,11 +29,37 @@ class AlbumListModel(application: Application) : GenericListModel(application) { // This way, we keep the scroll position val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!! - if (refresh || albumList.value!!.isEmpty() || albumListType != lastType) { + if (refresh || list.value!!.isEmpty() || albumListType != lastType) { lastType = albumListType backgroundLoadFromServer(refresh, swipe, args) } - return albumList + return list + } + + fun getAlbumsOfArtist(musicService: MusicService, refresh: Boolean, id: String, name: String?) { + + var root = MusicDirectory() + val musicDirectory = musicService.getArtist(id, name, refresh) + + if (Settings.shouldShowAllSongsByArtist && + musicDirectory.findChild(allSongsId) == null && + hasOnlyFolders(musicDirectory) + ) { + val allSongs = MusicDirectory.Entry(allSongsId) + + allSongs.isDirectory = true + allSongs.artist = name + allSongs.parent = id + allSongs.title = String.format( + context.resources.getString(R.string.select_album_all_songs), name + ) + + root.addFirst(allSongs) + root.addAll(musicDirectory.getChildren()) + } else { + root = musicDirectory + } + list.postValue(root.getChildren()) } override fun load( @@ -58,6 +87,15 @@ class AlbumListModel(application: Application) : GenericListModel(application) { // If appending the existing list, set the offset from where to load if (append) offset += (size + loadedUntil) + if (albumListType == Constants.ALBUMS_OF_ARTIST) { + return getAlbumsOfArtist( + musicService, + refresh, + args.getString(Constants.INTENT_EXTRA_NAME_ID, ""), + args.getString(Constants.INTENT_EXTRA_NAME_NAME, "") + ) + } + if (useId3Tags) { musicDirectory = musicService.getAlbumList2( albumListType, size, @@ -72,13 +110,13 @@ class AlbumListModel(application: Application) : GenericListModel(application) { currentListIsSortable = isCollectionSortable(albumListType) - if (append && albumList.value != null) { + if (append && list.value != null) { val list = ArrayList() - list.addAll(albumList.value!!) + list.addAll(this.list.value!!) list.addAll(musicDirectory.getAllChild()) - albumList.postValue(list) + this.list.postValue(list) } else { - albumList.postValue(musicDirectory.getAllChild()) + list.postValue(musicDirectory.getAllChild()) } loadedUntil = offset @@ -100,4 +138,5 @@ class AlbumListModel(application: Application) : GenericListModel(application) { albumListType != "highest" && albumListType != "recent" && albumListType != "frequent" } + } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt similarity index 98% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt index e87477b2..ca2bed1f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt @@ -16,7 +16,7 @@ Copyright 2020 (C) Jozsef Varga */ -package org.moire.ultrasonic.fragment +package org.moire.ultrasonic.model import android.app.Application import android.os.Bundle diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt similarity index 88% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt index 5ec1db0e..dd29662d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt @@ -1,4 +1,4 @@ -package org.moire.ultrasonic.fragment +package org.moire.ultrasonic.model import android.app.Application import android.content.Context @@ -17,6 +17,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting +import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicServiceFactory @@ -45,8 +46,6 @@ open class GenericListModel(application: Application) : return true } - internal val musicFolders: MutableLiveData> = MutableLiveData(listOf()) - /** * Helper function to check online status */ @@ -110,16 +109,20 @@ open class GenericListModel(application: Application) : ) { // Update the list of available folders if enabled if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) { - musicFolders.postValue( - musicService.getMusicFolders(refresh) - ) + //FIXME } } + /** - * Retrieves the available Music Folders in a LiveData + * Some shared helper functions */ - fun getMusicFolders(): LiveData> { - return musicFolders - } + + // Returns true if the directory contains only folders + internal fun hasOnlyFolders(musicDirectory: MusicDirectory) = + musicDirectory.getChildren(includeDirs = true, includeFiles = false).size == + musicDirectory.getChildren(includeDirs = true, includeFiles = true).size + + internal val allSongsId = "-1" + } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt new file mode 100644 index 00000000..e3c63086 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt @@ -0,0 +1,73 @@ +package org.moire.ultrasonic.model + +import android.app.Application +import android.os.Bundle +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.Identifiable +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.SearchCriteria +import org.moire.ultrasonic.domain.SearchResult +import org.moire.ultrasonic.fragment.SearchFragment +import org.moire.ultrasonic.service.MusicService +import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.util.BackgroundTask +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.FragmentBackgroundTask +import org.moire.ultrasonic.util.MergeAdapter +import org.moire.ultrasonic.util.Settings +import org.moire.ultrasonic.view.ArtistAdapter +import org.moire.ultrasonic.view.EntryAdapter +import java.util.ArrayList + +class SearchListModel(application: Application) : GenericListModel(application) { + + var searchResult: MutableLiveData = MutableLiveData(null) + + override fun load( + isOffline: Boolean, + useId3Tags: Boolean, + musicService: MusicService, + refresh: Boolean, + args: Bundle + ) { + super.load(isOffline, useId3Tags, musicService, refresh, args) + + + } + + + suspend fun search(query: String) { + val maxArtists = Settings.maxArtists + val maxAlbums = Settings.maxAlbums + val maxSongs = Settings.maxSongs + + withContext(Dispatchers.IO) { + val criteria = SearchCriteria(query, maxArtists, maxAlbums, maxSongs) + val service = MusicServiceFactory.getMusicService() + val result = service.search(criteria) + + if (result != null) searchResult.postValue(result) + } + } + + fun trimResultLength(result: SearchResult): SearchResult { + return SearchResult( + artists = result.artists.take(SearchFragment.DEFAULT_ARTISTS), + albums = result.albums.take(SearchFragment.DEFAULT_ALBUMS), + songs = result.songs.take(SearchFragment.DEFAULT_SONGS) + ) + } + +// fun mergeList(result: SearchResult): List { +// val list = mutableListOf() +// list.add(result.artists) +// list.add(result.albums) +// list.add(result.songs) +// return list +// } + +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt similarity index 99% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt index 65c2c6e6..2f520617 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt @@ -1,4 +1,4 @@ -package org.moire.ultrasonic.fragment +package org.moire.ultrasonic.model import android.app.Application import android.content.SharedPreferences diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt similarity index 80% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index 69a5b15d..3b097401 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -5,7 +5,7 @@ * Distributed under terms of the GNU GPLv3 license. */ -package org.moire.ultrasonic.fragment +package org.moire.ultrasonic.model import android.app.Application import android.os.Bundle @@ -22,25 +22,13 @@ import org.moire.ultrasonic.util.Util /* * Model for retrieving different collections of tracks from the API -* TODO: Refactor this model to extend the GenericListModel */ class TrackCollectionModel(application: Application) : GenericListModel(application) { - private val allSongsId = "-1" - val currentDirectory: MutableLiveData = MutableLiveData() val currentList: MutableLiveData> = MutableLiveData() val songsForGenre: MutableLiveData = MutableLiveData() - suspend fun getMusicFolders(refresh: Boolean) { - withContext(Dispatchers.IO) { - if (!isOffline()) { - val musicService = MusicServiceFactory.getMusicService() - musicFolders.postValue(musicService.getMusicFolders(refresh)) - } - } - } - suspend fun getMusicDirectory( refresh: Boolean, id: String, @@ -94,9 +82,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } } - private fun updateList(root: MusicDirectory) { - currentList.postValue(root.getChildren()) - } // Given a Music directory "songs" it recursively adds all children to "songs" private fun getSongsRecursively( @@ -122,42 +107,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } } - /* - * TODO: This method should be moved to AlbumListModel, - * since it displays a list of albums by a specified artist. - */ - suspend fun getArtist(refresh: Boolean, id: String, name: String?) { - - withContext(Dispatchers.IO) { - val service = MusicServiceFactory.getMusicService() - - var root = MusicDirectory() - - val musicDirectory = service.getArtist(id, name, refresh) - - if (Settings.shouldShowAllSongsByArtist && - musicDirectory.findChild(allSongsId) == null && - hasOnlyFolders(musicDirectory) - ) { - val allSongs = MusicDirectory.Entry(allSongsId) - - allSongs.isDirectory = true - allSongs.artist = name - allSongs.parent = id - allSongs.title = String.format( - context.resources.getString(R.string.select_album_all_songs), name - ) - - root.addFirst(allSongs) - root.addAll(musicDirectory.getChildren()) - } else { - root = musicDirectory - } - currentDirectory.postValue(root) - updateList(root) - } - } - suspend fun getAlbum(refresh: Boolean, id: String, name: String?, parentId: String?) { withContext(Dispatchers.IO) { @@ -296,18 +245,17 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } } - // Returns true if the directory contains only folders - private fun hasOnlyFolders(musicDirectory: MusicDirectory) = - musicDirectory.getChildren(includeDirs = true, includeFiles = false).size == - musicDirectory.getChildren(includeDirs = true, includeFiles = true).size - - override fun load( - isOffline: Boolean, - useId3Tags: Boolean, - musicService: MusicService, - refresh: Boolean, - args: Bundle - ) { - // See To_Do at the top + suspend fun getBookmarks() { + withContext(Dispatchers.IO) { + val service = MusicServiceFactory.getMusicService() + val musicDirectory = Util.getSongsFromBookmarks(service.getBookmarks()) + currentDirectory.postValue(musicDirectory) + updateList(musicDirectory) + } } + + private fun updateList(root: MusicDirectory) { + currentList.postValue(root.getChildren()) + } + } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index d7753662..fed88a9b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -399,7 +399,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, } @Throws(Exception::class) - override fun getBookmarks(): List? = musicService.getBookmarks() + override fun getBookmarks(): List = musicService.getBookmarks() @Throws(Exception::class) override fun deleteBookmark(id: String) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index 1a086d73..902ab3f9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -154,7 +154,7 @@ interface MusicService { fun addChatMessage(message: String) @Throws(Exception::class) - fun getBookmarks(): List? + fun getBookmarks(): List @Throws(Exception::class) fun deleteBookmark(id: String) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index d705119a..9ec622b7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -411,7 +411,7 @@ class OfflineMusicService : MusicService, KoinComponent { } @Throws(OfflineException::class) - override fun getBookmarks(): List? { + override fun getBookmarks(): List { throw OfflineException("getBookmarks isn't available in offline mode") } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index eeca3ffc..9ce64a0e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -30,6 +30,11 @@ class RxBus { val themeChangedEventObservable: Observable = themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread()) + val musicFolderChangedEventPublisher: PublishSubject = + PublishSubject.create() + val musicFolderChangedEventObservable: Observable = + musicFolderChangedEventPublisher.observeOn(AndroidSchedulers.mainThread()) + val playerStatePublisher: PublishSubject = PublishSubject.create() val playerStateObservable: Observable = @@ -73,6 +78,7 @@ class RxBus { val skipToQueueItemCommandObservable: Observable = skipToQueueItemCommandPublisher.observeOn(AndroidSchedulers.mainThread()) + fun releaseMediaSessionToken() { mediaSessionTokenPublisher = PublishSubject.create() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt index cde2df4c..ea604676 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -121,5 +121,6 @@ object Constants { const val ALBUM_ART_FILE = "folder.jpeg" const val STARRED = "starred" const val ALPHABETICAL_BY_NAME = "alphabeticalByName" + const val ALBUMS_OF_ARTIST = "albumsOfArtist" const val RESULT_CLOSE_ALL = 1337 } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt index 72e27f54..9f1efe0b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt @@ -4,7 +4,7 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper.DOWN import androidx.recyclerview.widget.ItemTouchHelper.UP import androidx.recyclerview.widget.RecyclerView -import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter +import org.moire.ultrasonic.adapters.BaseAdapter import timber.log.Timber class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { @@ -21,7 +21,7 @@ class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { Timber.w("MOVED %s %s", to, from) // Move it in the data set - (recyclerView.adapter as MultiTypeDiffAdapter<*>).moveItem(from, to) + (recyclerView.adapter as BaseAdapter<*>).moveItem(from, to) return true } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index 6851e09c..a8d798bf 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -525,11 +525,10 @@ object Util { } @JvmStatic - fun getSongsFromBookmarks(bookmarks: Iterable): MusicDirectory { + fun getSongsFromBookmarks(bookmarks: Iterable): MusicDirectory { val musicDirectory = MusicDirectory() var song: MusicDirectory.Entry for (bookmark in bookmarks) { - if (bookmark == null) continue song = bookmark.entry song.bookmarkPosition = bookmark.position musicDirectory.addChild(song) diff --git a/ultrasonic/src/main/res/layout/album_buttons.xml b/ultrasonic/src/main/res/layout/album_buttons.xml index 323ddd81..906f80d5 100644 --- a/ultrasonic/src/main/res/layout/album_buttons.xml +++ b/ultrasonic/src/main/res/layout/album_buttons.xml @@ -13,7 +13,8 @@ android:scaleType="fitCenter" android:layout_weight="1" android:src="?attr/select_all" - android:visibility="gone" /> + android:visibility="gone" + android:contentDescription="@string/common.select_all" /> + android:visibility="gone" + android:contentDescription="@string/common.play_now" /> + android:visibility="gone" + android:contentDescription="@string/common.play_next" /> + android:visibility="gone" + android:contentDescription="@string/common.play_last" /> + android:visibility="gone" + android:contentDescription="@string/common.pin" /> + android:visibility="gone" + android:contentDescription="@string/common.unpin" /> + android:visibility="gone" + android:contentDescription="@string/common.download" /> + android:visibility="gone" + android:contentDescription="@string/common.delete" /> + android:visibility="gone" + android:contentDescription="@string/search.more" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/search.xml b/ultrasonic/src/main/res/layout/search.xml index 5ef70eaa..a5132493 100644 --- a/ultrasonic/src/main/res/layout/search.xml +++ b/ultrasonic/src/main/res/layout/search.xml @@ -10,7 +10,7 @@ a:layout_height="0dip" a:layout_weight="1.0"> - - - - - - - - - - - - - - - - diff --git a/ultrasonic/src/main/res/navigation/navigation_graph.xml b/ultrasonic/src/main/res/navigation/navigation_graph.xml index c76a0720..096b5d1a 100644 --- a/ultrasonic/src/main/res/navigation/navigation_graph.xml +++ b/ultrasonic/src/main/res/navigation/navigation_graph.xml @@ -26,14 +26,14 @@ android:label="@string/music_library.label" > + app:destination="@id/albumListFragment" /> + app:destination="@id/albumListFragment" /> Play Shuffled Public Save + Select all Title Unpin Various Artists From eeb2d13d96b5e81c2cebd943b14574314c3a6a76 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 23 Nov 2021 21:58:58 +0100 Subject: [PATCH 10/33] Delete a bunch of now-unused classes Also run KtLint --- .../ultrasonic/fragment/SearchFragment.kt | 30 +- .../org/moire/ultrasonic/view/AlbumView.java | 181 -------- .../moire/ultrasonic/view/EntryAdapter.java | 144 ------- .../org/moire/ultrasonic/view/UpdateView.java | 155 ------- .../ultrasonic/adapters/AlbumRowBinder.kt | 5 +- .../ultrasonic/adapters/ArtistRowBinder.kt | 2 +- .../adapters/FolderSelectorBinder.kt | 8 +- .../org/moire/ultrasonic/adapters/Helper.kt | 2 +- .../ultrasonic/adapters/SectionedAdapter.kt | 18 - .../ultrasonic/adapters/TrackViewHolder.kt | 12 +- .../adapters/legacy/SongListAdapter.kt | 69 --- .../ultrasonic/fragment/AlbumListFragment.kt | 4 - .../ultrasonic/fragment/ArtistListFragment.kt | 5 +- .../ultrasonic/fragment/BookmarksFragment.kt | 32 +- .../ultrasonic/fragment/EntryListFragment.kt | 2 +- .../ultrasonic/fragment/MultiListFragment.kt | 5 +- .../fragment/TrackCollectionFragment.kt | 12 +- .../moire/ultrasonic/model/AlbumListModel.kt | 41 +- .../ultrasonic/model/GenericListModel.kt | 9 +- .../moire/ultrasonic/model/SearchListModel.kt | 17 +- .../ultrasonic/model/TrackCollectionModel.kt | 4 - .../org/moire/ultrasonic/service/RxBus.kt | 1 - .../ultrasonic/view/SelectMusicFolderView.kt | 87 ---- .../org/moire/ultrasonic/view/SongView.kt | 393 ------------------ .../moire/ultrasonic/view/SongViewHolder.kt | 316 -------------- 25 files changed, 64 insertions(+), 1490 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/view/EntryAdapter.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/view/UpdateView.java delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/SectionedAdapter.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/legacy/SongListAdapter.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SelectMusicFolderView.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt index 0bd32d29..4881396d 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -19,7 +19,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import androidx.navigation.Navigation -import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -41,7 +40,6 @@ import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util.toast import org.moire.ultrasonic.view.ArtistAdapter -import org.moire.ultrasonic.view.EntryAdapter import timber.log.Timber /** @@ -75,16 +73,16 @@ class SearchFragment : MultiListFragment(), KoinComponent { override val mainLayout: Int = R.layout.search - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) cancellationToken = CancellationToken() setTitle(this, R.string.search_title) setHasOptionsMenu(true) - - val buttons = LayoutInflater.from(context).inflate(R.layout.search_buttons, - listView, false) + val buttons = LayoutInflater.from(context).inflate( + R.layout.search_buttons, + listView, false + ) if (buttons != null) { artistsHeading = buttons.findViewById(R.id.search_artists) @@ -96,11 +94,12 @@ class SearchFragment : MultiListFragment(), KoinComponent { moreSongsButton = buttons.findViewById(R.id.search_more_songs) } - - listModel.searchResult.observe(viewLifecycleOwner, { - if (it != null) populateList(it) - }) - + listModel.searchResult.observe( + viewLifecycleOwner, + { + if (it != null) populateList(it) + } + ) searchRefresh = view.findViewById(R.id.search_entries_refresh) searchRefresh!!.isEnabled = false @@ -131,7 +130,6 @@ class SearchFragment : MultiListFragment(), KoinComponent { registerForContextMenu(listView!!) - viewAdapter.register( TrackViewBinder( checkable = false, @@ -149,7 +147,6 @@ class SearchFragment : MultiListFragment(), KoinComponent { ) ) - // Fragment was started with a query (e.g. from voice search), try to execute search right away val arguments = arguments if (arguments != null) { @@ -432,14 +429,14 @@ class SearchFragment : MultiListFragment(), KoinComponent { list.addAll(artists) if (artists.size > DEFAULT_ARTISTS) { // FIXME - //list.add((moreArtistsButton, true) + // list.add((moreArtistsButton, true) } } val albums = searchResult.albums if (albums.isNotEmpty()) { - //mergeAdapter!!.addView(albumsHeading) + // mergeAdapter!!.addView(albumsHeading) list.addAll(albums) - //mergeAdapter!!.addAdapter(albumAdapter) + // mergeAdapter!!.addAdapter(albumAdapter) // if (albums.size > DEFAULT_ALBUMS) { // moreAlbumsAdapter = mergeAdapter!!.addView(moreAlbumsButton, true) // } @@ -550,6 +547,5 @@ class SearchFragment : MultiListFragment(), KoinComponent { // FIXME override fun onItemClick(item: Identifiable) { - } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java deleted file mode 100644 index 17e85f98..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2009 (C) Sindre Mehus - */ -package org.moire.ultrasonic.view; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import timber.log.Timber; -import android.view.LayoutInflater; -import android.view.View; -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.data.ActiveServerProvider; -import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.service.MusicService; -import org.moire.ultrasonic.service.MusicServiceFactory; -import org.moire.ultrasonic.imageloader.ImageLoader; -import org.moire.ultrasonic.util.Settings; -import org.moire.ultrasonic.util.Util; - -/** - * Used to display albums in a {@code ListView}. - * - * @author Sindre Mehus - */ - - -public class AlbumView extends UpdateView -{ - private static Drawable starDrawable; - private static Drawable starHollowDrawable; - private static String theme; - - private final Context context; - private MusicDirectory.Entry entry; - private EntryAdapter.AlbumViewHolder viewHolder; - private final ImageLoader imageLoader; - private boolean maximized = false; - - public AlbumView(Context context, ImageLoader imageLoader) - { - super(context); - this.context = context; - this.imageLoader = imageLoader; - - String theme = Settings.getTheme(); - boolean themesMatch = theme.equals(AlbumView.theme); - AlbumView.theme = theme; - - if (starHollowDrawable == null || !themesMatch) - { - starHollowDrawable = Util.getDrawableFromAttribute(context, R.attr.star_hollow); - } - - if (starDrawable == null || !themesMatch) - { - starDrawable = Util.getDrawableFromAttribute(context, R.attr.star_full); - } - } - - public void setLayout() - { - LayoutInflater.from(context).inflate(R.layout.album_list_item_legacy, this, true); - viewHolder = new EntryAdapter.AlbumViewHolder(); - viewHolder.title = findViewById(R.id.album_title); - viewHolder.artist = findViewById(R.id.album_artist); - viewHolder.cover_art = findViewById(R.id.album_coverart); - viewHolder.star = findViewById(R.id.album_star); - setTag(viewHolder); - } - - public void setViewHolder(EntryAdapter.AlbumViewHolder viewHolder) - { - this.viewHolder = viewHolder; - this.viewHolder.cover_art.invalidate(); - setTag(this.viewHolder); - } - - public MusicDirectory.Entry getEntry() - { - return this.entry; - } - - public boolean isMaximized() { - return maximized; - } - - public void maximizeOrMinimize() { - maximized = !maximized; - if (this.viewHolder.title != null) { - this.viewHolder.title.setSingleLine(!maximized); - } - if (this.viewHolder.artist != null) { - this.viewHolder.artist.setSingleLine(!maximized); - } - } - - public void setAlbum(final MusicDirectory.Entry album) - { - viewHolder.cover_art.setTag(album); - imageLoader.loadImage(viewHolder.cover_art, album, false, 0); - this.entry = album; - - String title = album.getTitle(); - String artist = album.getArtist(); - boolean starred = album.getStarred(); - - viewHolder.title.setText(title); - viewHolder.artist.setText(artist); - viewHolder.artist.setVisibility(artist == null ? View.GONE : View.VISIBLE); - viewHolder.star.setImageDrawable(starred ? starDrawable : starHollowDrawable); - - if (ActiveServerProvider.Companion.isOffline() || "-1".equals(album.getId())) - { - viewHolder.star.setVisibility(View.GONE); - } - else - { - viewHolder.star.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(View view) - { - final boolean isStarred = album.getStarred(); - final String id = album.getId(); - - if (!isStarred) - { - viewHolder.star.setImageDrawable(starDrawable); - album.setStarred(true); - } - else - { - viewHolder.star.setImageDrawable(starHollowDrawable); - album.setStarred(false); - } - - final MusicService musicService = MusicServiceFactory.getMusicService(); - new Thread(new Runnable() - { - @Override - public void run() - { - boolean useId3 = Settings.getShouldUseId3Tags(); - - try - { - if (!isStarred) - { - musicService.star(!useId3 ? id : null, useId3 ? id : null, null); - } - else - { - musicService.unstar(!useId3 ? id : null, useId3 ? id : null, null); - } - } - catch (Exception e) - { - Timber.e(e); - } - } - }).start(); - } - }); - } - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/EntryAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/EntryAdapter.java deleted file mode 100644 index 65488e8f..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/EntryAdapter.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2010 (C) Sindre Mehus - */ -package org.moire.ultrasonic.view; - -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.CheckedTextView; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import org.moire.ultrasonic.domain.MusicDirectory.Entry; -import org.moire.ultrasonic.imageloader.ImageLoader; - -import java.util.List; - -/** - * This is the adapter for the display of a single list item (song, album, etc) - * - * @author Sindre Mehus - */ -public class EntryAdapter extends ArrayAdapter -{ - private final Context context; - private final ImageLoader imageLoader; - private final boolean checkable; - - public EntryAdapter(Context context, ImageLoader imageLoader, List entries, boolean checkable) - { - super(context, android.R.layout.simple_list_item_1, entries); - - this.context = context; - this.imageLoader = imageLoader; - this.checkable = checkable; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) - { - Entry entry = getItem(position); - - if (entry.isDirectory()) - { - AlbumView view; - - if (convertView instanceof AlbumView) - { - AlbumView currentView = (AlbumView) convertView; - - if (currentView.getEntry().equals(entry)) - { - return currentView; - } - else - { - AlbumViewHolder viewHolder = (AlbumViewHolder) currentView.getTag(); - view = currentView; - view.setViewHolder(viewHolder); - } - } - else - { - view = new AlbumView(context, imageLoader); - view.setLayout(); - } - - view.setAlbum(entry); - return view; - } - else - { - SongView view; - - if (convertView instanceof SongView) - { - SongView currentView = (SongView) convertView; - - if (currentView.getEntry().equals(entry)) - { - currentView.update(); - return currentView; - } - else - { - SongViewHolder viewHolder = (SongViewHolder) convertView.getTag(); - view = currentView; - view.setViewHolder(viewHolder); - } - } - else - { - view = new SongView(context); - view.setLayout(entry); - } - - view.setSong(entry, checkable, false); - return view; - } - } - - public static class SongViewHolder - { - CheckedTextView check; - TextView track; - TextView title; - TextView status; - TextView artist; - TextView duration; - LinearLayout rating; - ImageView fiveStar1; - ImageView fiveStar2; - ImageView fiveStar3; - ImageView fiveStar4; - ImageView fiveStar5; - ImageView star; - ImageView drag; - } - - public static class AlbumViewHolder - { - TextView artist; - ImageView cover_art; - ImageView star; - TextView title; - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/UpdateView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/UpdateView.java deleted file mode 100644 index cf94ab80..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/UpdateView.java +++ /dev/null @@ -1,155 +0,0 @@ -package org.moire.ultrasonic.view; - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.LinearLayout; - -import org.moire.ultrasonic.util.Settings; -import org.moire.ultrasonic.util.Util; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.WeakHashMap; - -import timber.log.Timber; - -/** - * A View that is periodically refreshed - * @deprecated - * Use LiveData to ensure that the content is up-to-date - **/ -public class UpdateView extends LinearLayout -{ - private static final WeakHashMap INSTANCES = new WeakHashMap(); - - private static Handler backgroundHandler; - private static Handler uiHandler; - private static Runnable updateRunnable; - - public UpdateView(Context context) - { - super(context); - - setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - INSTANCES.put(this, null); - startUpdater(); - } - - @Override - public void setPressed(boolean pressed) - { - - } - - private static synchronized void startUpdater() - { - if (uiHandler != null) - { - return; - } - - uiHandler = new Handler(); - updateRunnable = new Runnable() - { - @Override - public void run() - { - updateAll(); - } - }; - - new Thread(new Runnable() - { - @Override - public void run() - { - Thread.currentThread().setName("startUpdater"); - Looper.prepare(); - backgroundHandler = new Handler(Looper.myLooper()); - uiHandler.post(updateRunnable); - Looper.loop(); - } - }).start(); - } - - private static void updateAll() - { - try - { - Collection views = new ArrayList(); - - for (UpdateView view : INSTANCES.keySet()) - { - if (view.isShown()) - { - views.add(view); - } - } - - updateAllLive(views); - } - catch (Throwable x) - { - Timber.w(x, "Error when updating song views."); - } - } - - private static void updateAllLive(final Iterable views) - { - final Runnable runnable = new Runnable() - { - @Override - public void run() - { - try - { - for (UpdateView view : views) - { - view.update(); - } - } - catch (Throwable x) - { - Timber.w(x, "Error when updating song views."); - } - - uiHandler.postDelayed(updateRunnable, Settings.getViewRefreshInterval()); - } - }; - - backgroundHandler.post(new Runnable() - { - @Override - public void run() - { - try - { - Thread.currentThread().setName("updateAllLive-Background"); - - for (UpdateView view : views) - { - view.updateBackground(); - } - uiHandler.post(runnable); - } - catch (Throwable x) - { - Timber.w(x, "Error when updating song views."); - } - } - }); - } - - protected void updateBackground() - { - - } - - protected void update() - { - - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt index 6f2b7b2a..df0fa70e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt @@ -54,7 +54,7 @@ class AlbumRowBinder( val popup = Helper.createPopupMenu(holder.itemView) popup.setOnMenuItemClickListener { menuItem -> - onContextMenuClick(menuItem, item) + onContextMenuClick(menuItem, item) } true @@ -69,7 +69,6 @@ class AlbumRowBinder( ) } - /** * Holds the view properties of an Item row */ @@ -84,7 +83,6 @@ class AlbumRowBinder( var coverArtId: String? = null } - /** * Handles the star / unstar action for an album */ @@ -118,4 +116,3 @@ class AlbumRowBinder( return ViewHolder(inflater.inflate(layout, parent, false)) } } - diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt index 73aad3bf..f850e037 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt @@ -31,7 +31,7 @@ class ArtistRowBinder( val onItemClick: (ArtistOrIndex) -> Unit, val onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean, private val imageLoader: ImageLoader, -): ItemViewBinder(), KoinComponent { +) : ItemViewBinder(), KoinComponent { val layout = R.layout.artist_list_item val contextMenuLayout = R.menu.artist_context_menu diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt index 40e8290b..8b615b7d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt @@ -10,19 +10,20 @@ import android.widget.PopupMenu import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.drakeet.multitype.ItemViewBinder +import java.lang.ref.WeakReference import org.koin.core.component.KoinComponent import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.service.RxBus -import java.lang.ref.WeakReference /** * This little view shows the currently selected Folder (or catalog) on the music server. * When clicked it will drop down a list of all available Folders and allow you to * select one. The intended usage is to supply a filter to lists of artists, albums, etc */ -class FolderSelectorBinder(context: Context +class FolderSelectorBinder( + context: Context ) : ItemViewBinder(), KoinComponent { private val weakContext: WeakReference = WeakReference(context) @@ -112,7 +113,7 @@ class FolderSelectorBinder(context: Context data class FolderHeader( val folders: List, val selected: String? - ): Identifiable { + ) : Identifiable { override val id: String get() = "FOLDERSELECTOR" @@ -123,5 +124,4 @@ class FolderSelectorBinder(context: Context return longId.compareTo(other.longId) } } - } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt index ca510db5..9e371a00 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt @@ -19,4 +19,4 @@ object Helper { popup.show() return popup } -} \ No newline at end of file +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/SectionedAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/SectionedAdapter.kt deleted file mode 100644 index aabd48f1..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/SectionedAdapter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.moire.ultrasonic.adapters - -import com.drakeet.multitype.MultiTypeAdapter -import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView -import org.moire.ultrasonic.domain.Identifiable - -class SectionedAdapter : MultiTypeAdapter(), FastScrollRecyclerView.SectionedAdapter { - override fun getSectionName(position: Int): String { -// var listPosition = if (selectFolderHeader != null) position - 1 else position -// -// // Show the first artist's initial in the popup when the list is -// // scrolled up to the "Select Folder" row -// if (listPosition < 0) listPosition = 0 -// -// return getSectionFromName(currentList[listPosition].name ?: " ") - return "X" - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index e861836a..000b8614 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -13,7 +13,6 @@ import androidx.lifecycle.MutableLiveData 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 @@ -21,7 +20,6 @@ import org.moire.ultrasonic.featureflags.Feature import org.moire.ultrasonic.featureflags.FeatureStorage import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.DownloadStatus -import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.util.Settings @@ -130,10 +128,10 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable // Minimize or maximize the Text view (if song title is very long) itemView.setOnLongClickListener { - if (!song.isDirectory) { - maximizeOrMinimize() - true - } + if (!song.isDirectory) { + maximizeOrMinimize() + true + } false } } @@ -152,7 +150,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable } } - private fun setupStarButtons(song: MusicDirectory.Entry) { if (useFiveStarRating) { // Hide single star @@ -191,7 +188,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable } } - @Suppress("MagicNumber") private fun setFiveStars(rating: Int) { fiveStar1.setImageDrawable( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/legacy/SongListAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/legacy/SongListAdapter.kt deleted file mode 100644 index 3388a943..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/legacy/SongListAdapter.kt +++ /dev/null @@ -1,69 +0,0 @@ -package org.moire.ultrasonic.adapters.legacy - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import androidx.lifecycle.LifecycleOwner -import org.moire.ultrasonic.R -import org.moire.ultrasonic.adapters.ImageHelper -import org.moire.ultrasonic.adapters.TrackViewHolder -import org.moire.ultrasonic.service.DownloadFile - -/** - * Legacy bridge to provide Views to a ListView using RecyclerView.ViewHolders - */ -class SongListAdapter( - ctx: Context, - entries: List?, - val lifecycleOwner: LifecycleOwner -) : - ArrayAdapter(ctx, android.R.layout.simple_list_item_1, entries!!) { - - val layout = R.layout.song_list_item - private val imageHelper: ImageHelper = ImageHelper(context) - - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val downloadFile = getItem(position)!! - var view = convertView - val holder: TrackViewHolder - - if (view == null) { - val inflater = LayoutInflater.from(context) - view = inflater.inflate(layout, parent, false) - } - - if (view?.tag is TrackViewHolder) { - holder = view.tag as TrackViewHolder - } else { - holder = TrackViewHolder(view!!) - view.tag = holder - } - - holder.imageHelper = imageHelper - - holder.setSong( - file = downloadFile, - checkable = false, - draggable = true - ) - - // Observe download status - downloadFile.status.observe( - lifecycleOwner, - { - holder.updateStatus(it) - } - ) - - downloadFile.progress.observe( - lifecycleOwner, - { - holder.updateProgress(it) - } - ) - - return view - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index d2d29d49..cc11a99b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -73,7 +73,6 @@ class AlbumListFragment : EntryListFragment() { addOnScrollListener(scrollListener) } - viewAdapter.register( AlbumRowBinder( { entry -> onItemClick(entry) }, @@ -82,8 +81,6 @@ class AlbumListFragment : EntryListFragment() { context = requireContext() ) ) - - } override fun onItemClick(item: MusicDirectory.Entry) { @@ -94,5 +91,4 @@ class AlbumListFragment : EntryListFragment() { bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent) findNavController().navigate(itemClickTarget, bundle) } - } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 2b092a46..865843f2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -11,7 +11,6 @@ import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.model.ArtistListModel import org.moire.ultrasonic.util.Constants -import org.moire.ultrasonic.util.Settings /** * Displays the list of Artists from the media library @@ -44,7 +43,6 @@ class ArtistListFragment : EntryListFragment() { */ override val itemClickTarget = R.id.selectArtistToSelectAlbum - /** * The central function to pass a query to the model and return a LiveData object */ @@ -78,6 +76,5 @@ class ArtistListFragment : EntryListFragment() { findNavController().navigate(itemClickTarget, bundle) } - //Constants.ALPHABETICAL_BY_NAME - + // Constants.ALPHABETICAL_BY_NAME } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt index f0e2e4bd..e5431d35 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -26,8 +26,6 @@ class BookmarksFragment : TrackCollectionFragment() { // Why? selectButton?.visibility = View.GONE - playNextButton?.visibility = View.GONE - playLastButton?.visibility = View.GONE moreButton?.visibility = View.GONE } @@ -41,26 +39,14 @@ class BookmarksFragment : TrackCollectionFragment() { } override fun enableButtons(selection: List) { - val enabled = selection.isNotEmpty() - var unpinEnabled = false - var deleteEnabled = false - var pinnedCount = 0 - - for (song in selection) { - val downloadFile = mediaPlayerController.getDownloadFileForSong(song) - if (downloadFile.isWorkDone) { - deleteEnabled = true - } - if (downloadFile.isSaved) { - pinnedCount++ - unpinEnabled = true - } - } - - playNowButton?.isVisible = (enabled && deleteEnabled) - pinButton?.isVisible = (enabled && !isOffline() && selection.size > pinnedCount) - unpinButton!!.isVisible = (enabled && unpinEnabled) - downloadButton!!.isVisible = (enabled && !deleteEnabled && !isOffline()) - deleteButton!!.isVisible = (enabled && deleteEnabled) + super.enableButtons(selection) } } + + + + + + + + diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 5cef4c8f..af4f1947 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -24,7 +24,7 @@ abstract class EntryListFragment : MultiListFragment() { // FIXME fun showFolderHeader(): Boolean { return listModel.showSelectFolderHeader(arguments) && - !listModel.isOffline() && !Settings.shouldUseId3Tags + !listModel.isOffline() && !Settings.shouldUseId3Tags } @Suppress("LongMethod") diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index 22748d89..8d6725a7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -24,7 +24,6 @@ import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.Constants 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 @@ -38,7 +37,6 @@ abstract class MultiListFragment : Fragment() { 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 @@ -162,10 +160,9 @@ abstract class MultiListFragment : Fragment() { try { bundle = arguments?.clone() as Bundle } catch (ignored: Exception) { - bundle = Bundle() + bundle = Bundle() } return bundle } } - diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 18f8f577..f525fed2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.Navigation import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import java.util.Collections import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.launch import org.koin.android.ext.android.inject @@ -48,7 +49,6 @@ import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util import timber.log.Timber -import java.util.Collections /** * Displays a group of tracks, eg. the songs of an album, of a playlist etc. @@ -371,7 +371,7 @@ open class TrackCollectionFragment : MultiListFragment() { } } - val isArtist = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false)?: false + val isArtist = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false) ?: false // FIXME WHICH id if no arguments? val id = arguments?.getString(Constants.INTENT_EXTRA_NAME_ID) @@ -570,7 +570,7 @@ open class TrackCollectionFragment : MultiListFragment() { moreButton!!.visibility = View.GONE } else { moreButton!!.visibility = View.VISIBLE - if (arguments?.getInt(Constants.INTENT_EXTRA_NAME_RANDOM, 0) ?:0 > 0) { + if (arguments?.getInt(Constants.INTENT_EXTRA_NAME_RANDOM, 0) ?: 0 > 0) { moreButton!!.setOnClickListener { val offset = requireArguments().getInt( Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0 @@ -594,7 +594,7 @@ open class TrackCollectionFragment : MultiListFragment() { val isAlbumList = arguments?.containsKey( Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE - )?:false + ) ?: false playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos shareButtonVisible = !isOffline() && songCount > 0 @@ -615,11 +615,11 @@ open class TrackCollectionFragment : MultiListFragment() { viewAdapter.submitList(entryList) } - val playAll = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)?:false + val playAll = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) ?: false if (playAll && songCount > 0) { playAll( - arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)?:false, + arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false) ?: false, false ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt index a969afbd..6c28f94c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -14,8 +14,6 @@ import org.moire.ultrasonic.util.Settings class AlbumListModel(application: Application) : GenericListModel(application) { - - val list: MutableLiveData> = MutableLiveData(listOf()) var lastType: String? = null private var loadedUntil: Int = 0 @@ -38,28 +36,28 @@ class AlbumListModel(application: Application) : GenericListModel(application) { fun getAlbumsOfArtist(musicService: MusicService, refresh: Boolean, id: String, name: String?) { - var root = MusicDirectory() - val musicDirectory = musicService.getArtist(id, name, refresh) + var root = MusicDirectory() + val musicDirectory = musicService.getArtist(id, name, refresh) - if (Settings.shouldShowAllSongsByArtist && - musicDirectory.findChild(allSongsId) == null && - hasOnlyFolders(musicDirectory) - ) { - val allSongs = MusicDirectory.Entry(allSongsId) + if (Settings.shouldShowAllSongsByArtist && + musicDirectory.findChild(allSongsId) == null && + hasOnlyFolders(musicDirectory) + ) { + val allSongs = MusicDirectory.Entry(allSongsId) - allSongs.isDirectory = true - allSongs.artist = name - allSongs.parent = id - allSongs.title = String.format( - context.resources.getString(R.string.select_album_all_songs), name - ) + allSongs.isDirectory = true + allSongs.artist = name + allSongs.parent = id + allSongs.title = String.format( + context.resources.getString(R.string.select_album_all_songs), name + ) - root.addFirst(allSongs) - root.addAll(musicDirectory.getChildren()) - } else { - root = musicDirectory - } - list.postValue(root.getChildren()) + root.addFirst(allSongs) + root.addAll(musicDirectory.getChildren()) + } else { + root = musicDirectory + } + list.postValue(root.getChildren()) } override fun load( @@ -138,5 +136,4 @@ class AlbumListModel(application: Application) : GenericListModel(application) { albumListType != "highest" && albumListType != "recent" && albumListType != "frequent" } - } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt index dd29662d..497b9e9f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt @@ -6,8 +6,6 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import kotlinx.coroutines.Dispatchers @@ -18,7 +16,6 @@ import org.koin.core.component.inject import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.domain.MusicDirectory -import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.CommunicationError @@ -109,11 +106,10 @@ open class GenericListModel(application: Application) : ) { // Update the list of available folders if enabled if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) { - //FIXME + // FIXME } } - /** * Some shared helper functions */ @@ -121,8 +117,7 @@ open class GenericListModel(application: Application) : // Returns true if the directory contains only folders internal fun hasOnlyFolders(musicDirectory: MusicDirectory) = musicDirectory.getChildren(includeDirs = true, includeFiles = false).size == - musicDirectory.getChildren(includeDirs = true, includeFiles = true).size + musicDirectory.getChildren(includeDirs = true, includeFiles = true).size internal val allSongsId = "-1" - } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt index e3c63086..38a30901 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt @@ -2,26 +2,15 @@ package org.moire.ultrasonic.model import android.app.Application import android.os.Bundle -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.moire.ultrasonic.domain.Artist -import org.moire.ultrasonic.domain.Identifiable -import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.fragment.SearchFragment import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicServiceFactory -import org.moire.ultrasonic.util.BackgroundTask -import org.moire.ultrasonic.util.Constants -import org.moire.ultrasonic.util.FragmentBackgroundTask -import org.moire.ultrasonic.util.MergeAdapter import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.view.ArtistAdapter -import org.moire.ultrasonic.view.EntryAdapter -import java.util.ArrayList class SearchListModel(application: Application) : GenericListModel(application) { @@ -35,11 +24,8 @@ class SearchListModel(application: Application) : GenericListModel(application) args: Bundle ) { super.load(isOffline, useId3Tags, musicService, refresh, args) - - } - suspend fun search(query: String) { val maxArtists = Settings.maxArtists val maxAlbums = Settings.maxAlbums @@ -69,5 +55,4 @@ class SearchListModel(application: Application) : GenericListModel(application) // list.add(result.songs) // return list // } - -} \ No newline at end of file +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index 3b097401..ee5b4e27 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -8,14 +8,12 @@ package org.moire.ultrasonic.model import android.app.Application -import android.os.Bundle import androidx.lifecycle.MutableLiveData import java.util.LinkedList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.MusicDirectory -import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util @@ -82,7 +80,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } } - // Given a Music directory "songs" it recursively adds all children to "songs" private fun getSongsRecursively( parent: MusicDirectory, @@ -257,5 +254,4 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat private fun updateList(root: MusicDirectory) { currentList.postValue(root.getChildren()) } - } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index 9ce64a0e..f5686372 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -78,7 +78,6 @@ class RxBus { val skipToQueueItemCommandObservable: Observable = skipToQueueItemCommandPublisher.observeOn(AndroidSchedulers.mainThread()) - fun releaseMediaSessionToken() { mediaSessionTokenPublisher = PublishSubject.create() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SelectMusicFolderView.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SelectMusicFolderView.kt deleted file mode 100644 index 3dcec9a4..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SelectMusicFolderView.kt +++ /dev/null @@ -1,87 +0,0 @@ -package org.moire.ultrasonic.view - -import android.content.Context -import android.view.MenuItem -import android.view.View -import android.widget.LinearLayout -import android.widget.PopupMenu -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import org.moire.ultrasonic.R -import org.moire.ultrasonic.domain.MusicFolder - -/** - * This little view shows the currently selected Folder (or catalog) on the music server. - * When clicked it will drop down a list of all available Folders and allow you to - * select one. The intended usage is to supply a filter to lists of artists, albums, etc - */ -class SelectMusicFolderView( - private val context: Context, - view: View, - private val onUpdate: (String?) -> Unit -) : RecyclerView.ViewHolder(view) { - private var musicFolders: List = mutableListOf() - private var selectedFolderId: String? = null - private val folderName: TextView = itemView.findViewById(R.id.select_folder_name) - private val layout: LinearLayout = itemView.findViewById(R.id.select_folder_header) - - init { - folderName.text = context.getString(R.string.select_artist_all_folders) - layout.setOnClickListener { onFolderClick() } - } - - fun setData(selectedId: String?, folders: List) { - selectedFolderId = selectedId - musicFolders = folders - if (selectedFolderId != null) { - for ((id, name) in musicFolders) { - if (id == selectedFolderId) { - folderName.text = name - break - } - } - } else { - folderName.text = context.getString(R.string.select_artist_all_folders) - } - } - - private fun onFolderClick() { - val popup = PopupMenu(context, layout) - - var menuItem = popup.menu.add( - MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders - ) - if (selectedFolderId == null || selectedFolderId!!.isEmpty()) { - menuItem.isChecked = true - } - musicFolders.forEachIndexed { i, musicFolder -> - val (id, name) = musicFolder - menuItem = popup.menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, name) - if (id == selectedFolderId) { - menuItem.isChecked = true - } - } - - popup.menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true) - - popup.setOnMenuItemClickListener { item -> onFolderMenuItemSelected(item) } - popup.show() - } - - private fun onFolderMenuItemSelected(menuItem: MenuItem): Boolean { - val selectedFolder = if (menuItem.itemId == -1) null else musicFolders[menuItem.itemId] - val musicFolderName = selectedFolder?.name - ?: context.getString(R.string.select_artist_all_folders) - selectedFolderId = selectedFolder?.id - - menuItem.isChecked = true - folderName.text = musicFolderName - onUpdate(selectedFolderId) - - return true - } - - companion object { - const val MENU_GROUP_MUSIC_FOLDER = 10 - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt deleted file mode 100644 index 651926fe..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt +++ /dev/null @@ -1,393 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2020 (C) Jozsef Varga - */ -package org.moire.ultrasonic.view - -import android.content.Context -import android.graphics.drawable.AnimationDrawable -import android.graphics.drawable.Drawable -import android.text.TextUtils -import android.view.LayoutInflater -import android.widget.Checkable -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.Companion.isOffline -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.getMusicService -import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.util.Util -import org.moire.ultrasonic.view.EntryAdapter.SongViewHolder -import timber.log.Timber - -/** - * Used to display songs and videos in a `ListView`. - */ -class SongView(context: Context) : UpdateView(context), Checkable, KoinComponent { - - var entry: MusicDirectory.Entry? = 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 downloadFile: DownloadFile? = null - private var playing = false - private var viewHolder: SongViewHolder? = null - private val features: FeatureStorage = get() - private val useFiveStarRating: Boolean = features.isFeatureEnabled(Feature.FIVE_STAR_RATING) - private val mediaPlayerController: MediaPlayerController by inject() - - fun setLayout(song: MusicDirectory.Entry) { - - inflater?.inflate( - if (song.isVideo) R.layout.video_list_item - else R.layout.song_list_item, - this, - true - ) - - viewHolder = SongViewHolder() - viewHolder!!.check = findViewById(R.id.song_check) - viewHolder!!.rating = findViewById(R.id.song_rating) - viewHolder!!.fiveStar1 = findViewById(R.id.song_five_star_1) - viewHolder!!.fiveStar2 = findViewById(R.id.song_five_star_2) - viewHolder!!.fiveStar3 = findViewById(R.id.song_five_star_3) - viewHolder!!.fiveStar4 = findViewById(R.id.song_five_star_4) - viewHolder!!.fiveStar5 = findViewById(R.id.song_five_star_5) - viewHolder!!.star = findViewById(R.id.song_star) - viewHolder!!.drag = findViewById(R.id.song_drag) - viewHolder!!.track = findViewById(R.id.song_track) - viewHolder!!.title = findViewById(R.id.song_title) - viewHolder!!.artist = findViewById(R.id.song_artist) - viewHolder!!.duration = findViewById(R.id.song_duration) - viewHolder!!.status = findViewById(R.id.song_status) - tag = viewHolder - } - - fun setViewHolder(viewHolder: SongViewHolder?) { - this.viewHolder = viewHolder - tag = this.viewHolder - } - - fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean) { - updateBackground() - - entry = song - downloadFile = mediaPlayerController.getDownloadFileForSong(song) - - val artist = StringBuilder(60) - var bitRate: String? = null - - if (song.bitRate != null) - bitRate = String.format( - this.context.getString(R.string.song_details_kbps), song.bitRate - ) - - val fileFormat: String? - val suffix = song.suffix - val transcodedSuffix = song.transcodedSuffix - - fileFormat = if ( - TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo - ) suffix else String.format("%s > %s", suffix, transcodedSuffix) - - val artistName = song.artist - - if (artistName != null) { - if (Settings.shouldDisplayBitrateWithArtist) { - artist.append(artistName).append(" (").append( - String.format( - this.context.getString(R.string.song_details_all), - if (bitRate == null) "" else String.format("%s ", bitRate), fileFormat - ) - ).append(')') - } else { - artist.append(artistName) - } - } - - val trackNumber = song.track ?: 0 - - if (Settings.shouldShowTrackNumber && trackNumber != 0) { - viewHolder?.track?.text = String.format("%02d.", trackNumber) - } else { - viewHolder?.track?.visibility = GONE - } - - val title = StringBuilder(60) - title.append(song.title) - - if (song.isVideo && Settings.shouldDisplayBitrateWithArtist) { - title.append(" (").append( - String.format( - this.context.getString(R.string.song_details_all), - if (bitRate == null) "" else String.format("%s ", bitRate), fileFormat - ) - ).append(')') - } - - viewHolder?.title?.text = title - viewHolder?.artist?.text = artist - - val duration = song.duration - if (duration != null) { - viewHolder?.duration?.text = Util.formatTotalDuration(duration.toLong()) - } - - viewHolder?.check?.visibility = if (checkable && !song.isVideo) VISIBLE else GONE - viewHolder?.drag?.visibility = if (draggable) VISIBLE else GONE - - if (isOffline()) { - viewHolder?.star?.visibility = GONE - viewHolder?.rating?.visibility = GONE - } else { - if (useFiveStarRating) { - viewHolder?.star?.visibility = GONE - val rating = if (song.userRating == null) 0 else song.userRating!! - viewHolder?.fiveStar1?.setImageDrawable( - if (rating > 0) starDrawable else starHollowDrawable - ) - viewHolder?.fiveStar2?.setImageDrawable( - if (rating > 1) starDrawable else starHollowDrawable - ) - viewHolder?.fiveStar3?.setImageDrawable( - if (rating > 2) starDrawable else starHollowDrawable - ) - viewHolder?.fiveStar4?.setImageDrawable( - if (rating > 3) starDrawable else starHollowDrawable - ) - viewHolder?.fiveStar5?.setImageDrawable( - if (rating > 4) starDrawable else starHollowDrawable - ) - } else { - viewHolder?.rating?.visibility = GONE - viewHolder?.star?.setImageDrawable( - if (song.starred) starDrawable else starHollowDrawable - ) - - viewHolder?.star?.setOnClickListener { - val isStarred = song.starred - val id = song.id - - if (!isStarred) { - viewHolder?.star?.setImageDrawable(starDrawable) - song.starred = true - } else { - viewHolder?.star?.setImageDrawable(starHollowDrawable) - song.starred = false - } - Thread { - val musicService = getMusicService() - try { - if (!isStarred) { - musicService.star(id, null, null) - } else { - musicService.unstar(id, null, null) - } - } catch (e: Exception) { - Timber.e(e) - } - }.start() - } - } - } - update() - } - - override fun updateBackground() {} - - @Synchronized - public override fun update() { - updateBackground() - - val song = entry ?: return - - downloadFile = mediaPlayerController.getDownloadFileForSong(song) - - updateDownloadStatus(downloadFile!!) - - if (entry?.starred != true) { - if (viewHolder?.star?.drawable !== starHollowDrawable) { - viewHolder?.star?.setImageDrawable(starHollowDrawable) - } - } else { - if (viewHolder?.star?.drawable !== starDrawable) { - viewHolder?.star?.setImageDrawable(starDrawable) - } - } - - val rating = entry?.userRating ?: 0 - viewHolder?.fiveStar1?.setImageDrawable( - if (rating > 0) starDrawable else starHollowDrawable - ) - viewHolder?.fiveStar2?.setImageDrawable( - if (rating > 1) starDrawable else starHollowDrawable - ) - viewHolder?.fiveStar3?.setImageDrawable( - if (rating > 2) starDrawable else starHollowDrawable - ) - viewHolder?.fiveStar4?.setImageDrawable( - if (rating > 3) starDrawable else starHollowDrawable - ) - viewHolder?.fiveStar5?.setImageDrawable( - if (rating > 4) starDrawable else starHollowDrawable - ) - - val playing = mediaPlayerController.currentPlaying === downloadFile - - if (playing) { - if (!this.playing) { - this.playing = true - viewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds( - playingImage, null, null, null - ) - } - } else { - if (this.playing) { - this.playing = false - viewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds( - 0, 0, 0, 0 - ) - } - } - } - - private fun updateDownloadStatus(downloadFile: DownloadFile) { - - if (downloadFile.isWorkDone) { - val newLeftImageType = - if (downloadFile.isSaved) ImageType.Pin else ImageType.Downloaded - - if (leftImageType != newLeftImageType) { - leftImage = if (downloadFile.isSaved) pinImage else downloadedImage - leftImageType = newLeftImageType - } - } else { - leftImageType = ImageType.None - leftImage = null - } - - val rightImageType: ImageType - val rightImage: Drawable? - - if (downloadFile.isDownloading && !downloadFile.isDownloadCancelled) { - viewHolder?.status?.text = Util.formatPercentage(downloadFile.progress.value!!) - - rightImageType = ImageType.Downloading - rightImage = downloadingImage - } else { - rightImageType = ImageType.None - rightImage = null - - val statusText = viewHolder?.status?.text - if (!statusText.isNullOrEmpty()) viewHolder?.status?.text = null - } - - if (previousLeftImageType != leftImageType || previousRightImageType != rightImageType) { - previousLeftImageType = leftImageType - previousRightImageType = rightImageType - - if (viewHolder?.status != null) { - viewHolder?.status?.setCompoundDrawablesWithIntrinsicBounds( - leftImage, null, rightImage, null - ) - - if (rightImage === downloadingImage) { - val frameAnimation = rightImage as AnimationDrawable? - - frameAnimation!!.setVisible(true, true) - frameAnimation.start() - } - } - } - } - - override fun setChecked(b: Boolean) { - viewHolder?.check?.isChecked = b - } - - override fun isChecked(): Boolean { - return viewHolder?.check?.isChecked ?: false - } - - override fun toggle() { - viewHolder?.check?.toggle() - } - - fun maximizeOrMinimize() { - isMaximized = !isMaximized - - viewHolder?.title?.isSingleLine = !isMaximized - viewHolder?.artist?.isSingleLine = !isMaximized - } - - enum class ImageType { - None, Pin, Downloaded, Downloading - } - - companion object { - private var starHollowDrawable: Drawable? = null - private var starDrawable: Drawable? = null - var pinImage: Drawable? = null - var downloadedImage: Drawable? = null - var downloadingImage: Drawable? = null - private var playingImage: Drawable? = null - private var theme: String? = null - private var inflater: LayoutInflater? = null - } - - init { - val theme = Settings.theme - val themesMatch = theme == Companion.theme - inflater = LayoutInflater.from(this.context) - - if (!themesMatch) Companion.theme = theme - - if (starHollowDrawable == null || !themesMatch) { - starHollowDrawable = Util.getDrawableFromAttribute(context, R.attr.star_hollow) - } - - if (starDrawable == null || !themesMatch) { - starDrawable = Util.getDrawableFromAttribute(context, R.attr.star_full) - } - - if (pinImage == null || !themesMatch) { - pinImage = Util.getDrawableFromAttribute(context, R.attr.pin) - } - - if (downloadedImage == null || !themesMatch) { - downloadedImage = Util.getDrawableFromAttribute(context, R.attr.downloaded) - } - - if (downloadingImage == null || !themesMatch) { - downloadingImage = Util.getDrawableFromAttribute(context, R.attr.downloading) - } - - if (playingImage == null || !themesMatch) { - playingImage = Util.getDrawableFromAttribute(context, R.attr.media_play_small) - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt deleted file mode 100644 index 51357900..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongViewHolder.kt +++ /dev/null @@ -1,316 +0,0 @@ -// 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 -// } -// -// -// } From 2086a6cac5c70cf6b1cec8d87a22ec8a965c0e37 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 23 Nov 2021 23:54:34 +0100 Subject: [PATCH 11/33] Unify play next and play last icons, style fixes --- .../org/moire/ultrasonic/fragment/SearchFragment.kt | 10 +++++++--- .../org/moire/ultrasonic/adapters/BaseAdapter.kt | 2 +- .../moire/ultrasonic/adapters/FolderSelectorBinder.kt | 6 +++--- .../kotlin/org/moire/ultrasonic/adapters/Helper.kt | 4 ++-- .../moire/ultrasonic/fragment/ArtistListFragment.kt | 3 ++- .../org/moire/ultrasonic/model/SearchListModel.kt | 8 -------- .../org/moire/ultrasonic/util/DragSortCallback.kt | 4 +--- .../src/main/res/drawable/ic_baseline_info_24.xml | 10 ++++++++++ .../main/res/drawable/ic_menu_add_to_queue_dark.xml | 9 --------- .../main/res/drawable/ic_menu_add_to_queue_light.xml | 9 --------- ultrasonic/src/main/res/drawable/ic_play_last.xml | 10 ++++++++++ ultrasonic/src/main/res/drawable/ic_play_next.xml | 10 ++++++++++ .../src/main/res/drawable/media_play_next_dark.xml | 9 --------- .../src/main/res/drawable/media_play_next_light.xml | 9 --------- ultrasonic/src/main/res/layout/album_buttons.xml | 4 ++-- ultrasonic/src/main/res/values/styles.xml | 2 -- ultrasonic/src/main/res/values/themes.xml | 9 +++------ 17 files changed, 51 insertions(+), 67 deletions(-) create mode 100644 ultrasonic/src/main/res/drawable/ic_baseline_info_24.xml delete mode 100644 ultrasonic/src/main/res/drawable/ic_menu_add_to_queue_dark.xml delete mode 100644 ultrasonic/src/main/res/drawable/ic_menu_add_to_queue_light.xml create mode 100644 ultrasonic/src/main/res/drawable/ic_play_last.xml create mode 100644 ultrasonic/src/main/res/drawable/ic_play_next.xml delete mode 100644 ultrasonic/src/main/res/drawable/media_play_next_dark.xml delete mode 100644 ultrasonic/src/main/res/drawable/media_play_next_light.xml diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt index 4881396d..183f7410 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -167,7 +167,9 @@ class SearchFragment : MultiListFragment(), KoinComponent { inflater.inflate(R.menu.search, menu) val searchItem = menu.findItem(R.id.search_item) val searchView = searchItem.actionView as SearchView - searchView.setSearchableInfo(searchManager.getSearchableInfo(requireActivity().componentName)) + val searchableInfo = searchManager.getSearchableInfo(requireActivity().componentName) + searchView.setSearchableInfo(searchableInfo) + val arguments = arguments val autoPlay = arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) @@ -186,8 +188,10 @@ class SearchFragment : MultiListFragment(), KoinComponent { Timber.d("onSuggestionClick: %d", position) val cursor = searchView.suggestionsAdapter.cursor cursor.moveToPosition(position) - val suggestion = - cursor.getString(2) // TODO: Try to do something with this magic const -- 2 is the index of col containing suggestion name. + + // TODO: Try to do something with this magic const: + // 2 is the index of col containing suggestion name. + val suggestion = cursor.getString(2) searchView.setQuery(suggestion, true) return true } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index 9cf73253..804cb308 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -33,7 +33,7 @@ class BaseAdapter : MultiTypeAdapter() { override var items: List get() = getCurrentList() set(value) { - throw IllegalAccessException("You must use submitList() to add data to the MultiTypeDiffAdapter") + throw IllegalAccessException("You must use submitList() to add data to the Adapter") } var mDiffer: AsyncListDiffer = AsyncListDiffer( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt index 8b615b7d..83880ee8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt @@ -22,9 +22,9 @@ import org.moire.ultrasonic.service.RxBus * When clicked it will drop down a list of all available Folders and allow you to * select one. The intended usage is to supply a filter to lists of artists, albums, etc */ -class FolderSelectorBinder( - context: Context -) : ItemViewBinder(), KoinComponent { +class FolderSelectorBinder(context: Context) : + ItemViewBinder(), + KoinComponent { private val weakContext: WeakReference = WeakReference(context) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt index 9e371a00..4ed76b0a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt @@ -8,10 +8,10 @@ import org.moire.ultrasonic.data.ActiveServerProvider object Helper { @JvmStatic - fun createPopupMenu(view: View, contextMenuLayout: Int = R.menu.artist_context_menu): PopupMenu { + fun createPopupMenu(view: View, layout: Int = R.menu.artist_context_menu): PopupMenu { val popup = PopupMenu(view.context, view) val inflater: MenuInflater = popup.menuInflater - inflater.inflate(contextMenuLayout, popup.menu) + inflater.inflate(layout, popup.menu) val downloadMenuItem = popup.menu.findItem(R.id.menu_download) downloadMenuItem?.isVisible = !ActiveServerProvider.isOffline() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 865843f2..10b9c7ee 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -69,7 +69,8 @@ class ArtistListFragment : EntryListFragment() { 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)) - bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, Constants.ALPHABETICAL_BY_NAME) + bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, + Constants.ALPHABETICAL_BY_NAME) bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt index 38a30901..7bd1c3a4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt @@ -47,12 +47,4 @@ class SearchListModel(application: Application) : GenericListModel(application) songs = result.songs.take(SearchFragment.DEFAULT_SONGS) ) } - -// fun mergeList(result: SearchResult): List { -// val list = mutableListOf() -// list.add(result.artists) -// list.add(result.albums) -// list.add(result.songs) -// return list -// } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt index 9f1efe0b..91894874 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt @@ -18,9 +18,7 @@ class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { val from = viewHolder.bindingAdapterPosition val to = target.bindingAdapterPosition - Timber.w("MOVED %s %s", to, from) - - // Move it in the data set + // FIXME: Move it in the data set (recyclerView.adapter as BaseAdapter<*>).moveItem(from, to) return true diff --git a/ultrasonic/src/main/res/drawable/ic_baseline_info_24.xml b/ultrasonic/src/main/res/drawable/ic_baseline_info_24.xml new file mode 100644 index 00000000..17255b7a --- /dev/null +++ b/ultrasonic/src/main/res/drawable/ic_baseline_info_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/ic_menu_add_to_queue_dark.xml b/ultrasonic/src/main/res/drawable/ic_menu_add_to_queue_dark.xml deleted file mode 100644 index 157454a8..00000000 --- a/ultrasonic/src/main/res/drawable/ic_menu_add_to_queue_dark.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/ic_menu_add_to_queue_light.xml b/ultrasonic/src/main/res/drawable/ic_menu_add_to_queue_light.xml deleted file mode 100644 index d330ca57..00000000 --- a/ultrasonic/src/main/res/drawable/ic_menu_add_to_queue_light.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/ic_play_last.xml b/ultrasonic/src/main/res/drawable/ic_play_last.xml new file mode 100644 index 00000000..156f3ce6 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/ic_play_last.xml @@ -0,0 +1,10 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/ic_play_next.xml b/ultrasonic/src/main/res/drawable/ic_play_next.xml new file mode 100644 index 00000000..cdd599eb --- /dev/null +++ b/ultrasonic/src/main/res/drawable/ic_play_next.xml @@ -0,0 +1,10 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/media_play_next_dark.xml b/ultrasonic/src/main/res/drawable/media_play_next_dark.xml deleted file mode 100644 index b7d80429..00000000 --- a/ultrasonic/src/main/res/drawable/media_play_next_dark.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/media_play_next_light.xml b/ultrasonic/src/main/res/drawable/media_play_next_light.xml deleted file mode 100644 index 3e7cedf9..00000000 --- a/ultrasonic/src/main/res/drawable/media_play_next_light.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/layout/album_buttons.xml b/ultrasonic/src/main/res/layout/album_buttons.xml index 906f80d5..73acfcd0 100644 --- a/ultrasonic/src/main/res/layout/album_buttons.xml +++ b/ultrasonic/src/main/res/layout/album_buttons.xml @@ -32,7 +32,7 @@ android:layout_height="32dp" android:scaleType="fitCenter" android:layout_weight="1" - android:src="?attr/media_play_next" + android:src="@drawable/ic_play_next" android:visibility="gone" android:contentDescription="@string/common.play_next" /> @@ -42,7 +42,7 @@ android:layout_height="32dp" android:scaleType="fitCenter" android:layout_weight="1" - android:src="?attr/add_to_queue" + android:src="@drawable/ic_play_last" android:visibility="gone" android:contentDescription="@string/common.play_last" /> diff --git a/ultrasonic/src/main/res/values/styles.xml b/ultrasonic/src/main/res/values/styles.xml index f71b926c..d9cfaa1d 100644 --- a/ultrasonic/src/main/res/values/styles.xml +++ b/ultrasonic/src/main/res/values/styles.xml @@ -39,7 +39,6 @@ - @@ -73,7 +72,6 @@ - diff --git a/ultrasonic/src/main/res/values/themes.xml b/ultrasonic/src/main/res/values/themes.xml index bf6a0b1b..42a75fd3 100644 --- a/ultrasonic/src/main/res/values/themes.xml +++ b/ultrasonic/src/main/res/values/themes.xml @@ -12,7 +12,6 @@ @drawable/ic_star_full_dark @drawable/ic_menu_about_dark @drawable/ic_menu_select_all_dark - @drawable/ic_menu_add_to_queue_dark @drawable/ic_menu_browse_dark @drawable/ic_menu_exit_dark @drawable/ic_menu_backward_dark @@ -46,7 +45,6 @@ @drawable/media_start_normal_dark @drawable/ic_menu_podcasts_dark @drawable/ic_menu_refresh_dark - @drawable/media_play_next_dark @drawable/ic_stat_play_dark @drawable/media_stop_normal_dark @drawable/media_toggle_list_normal_dark @@ -63,6 +61,7 @@ @drawable/ic_more_vert_dark @drawable/list_selector_holo_dark @drawable/list_selector_holo_dark_selected + @color/selected_menu_background_light \ No newline at end of file From ad793e27a50f9d6de220c37a929e486fff905065 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 25 Nov 2021 19:47:45 +0100 Subject: [PATCH 12/33] Remove viewRefresh setting --- .../ultrasonic/fragment/SettingsFragment.kt | 3 --- ultrasonic/src/main/res/values-cs/strings.xml | 11 --------- ultrasonic/src/main/res/values-de/strings.xml | 11 --------- ultrasonic/src/main/res/values-es/strings.xml | 11 --------- ultrasonic/src/main/res/values-fr/strings.xml | 11 --------- ultrasonic/src/main/res/values-hu/strings.xml | 11 --------- ultrasonic/src/main/res/values-it/strings.xml | 10 -------- ultrasonic/src/main/res/values-nl/strings.xml | 11 --------- ultrasonic/src/main/res/values-pl/strings.xml | 11 --------- .../src/main/res/values-pt-rBR/strings.xml | 11 --------- ultrasonic/src/main/res/values-pt/strings.xml | 11 --------- ultrasonic/src/main/res/values-ru/strings.xml | 11 --------- .../src/main/res/values-zh-rCN/strings.xml | 11 --------- ultrasonic/src/main/res/values/arrays.xml | 24 ------------------- ultrasonic/src/main/res/values/strings.xml | 11 --------- ultrasonic/src/main/res/xml/settings.xml | 7 ------ 16 files changed, 176 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index a2ee1531..789fde4c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -81,7 +81,6 @@ class SettingsFragment : private var sendBluetoothNotifications: CheckBoxPreference? = null private var sendBluetoothAlbumArt: CheckBoxPreference? = null private var showArtistPicture: CheckBoxPreference? = null - private var viewRefresh: ListPreference? = null private var sharingDefaultDescription: EditTextPreference? = null private var sharingDefaultGreeting: EditTextPreference? = null private var sharingDefaultExpiration: TimeSpanPreference? = null @@ -130,7 +129,6 @@ class SettingsFragment : sendBluetoothAlbumArt = findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART) sendBluetoothNotifications = findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS) - viewRefresh = findPreference(Constants.PREFERENCES_KEY_VIEW_REFRESH) sharingDefaultDescription = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION) sharingDefaultGreeting = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_GREETING) @@ -371,7 +369,6 @@ class SettingsFragment : defaultSongs!!.summary = defaultSongs!!.entry chatRefreshInterval!!.summary = chatRefreshInterval!!.entry directoryCacheTime!!.summary = directoryCacheTime!!.entry - viewRefresh!!.summary = viewRefresh!!.entry sharingDefaultExpiration!!.summary = sharingDefaultExpiration!!.text sharingDefaultDescription!!.summary = sharingDefaultDescription!!.text sharingDefaultGreeting!!.summary = sharingDefaultGreeting!!.text diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index 4ae56ac0..06986573 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -303,17 +303,6 @@ Obrázek umělce v seznamu umělců Zobrazí obrázek umělce v náhledu umělců pokud je dostupný Video - Obnovení náhledu - .5 sekundy - 1 sekunda - 1.5 sekundy - 2 sekundy - 2.5 sekundy - 3 sekundy - 3.5 sekundy - 4 sekundy - 4.5 sekundy - 5 sekund Streamovat media pouze přes Wi-Fi připojení Streamovat pouze přes Wi-Fi %1$s%2$s diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index e7f62174..878f130d 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -298,17 +298,6 @@ Durchsuchen von ID3-Tags Nutze ID3 Tag Methode anstatt Dateisystem-Methode Film - Aktualisierungsinterval - .5 Sekunden - 1 Sekunde - 1.5 Sekunden - 2 Sekunden - 2.5 Sekunden - 3 Sekunden - 3.5 Sekunden - 4 Sekunden - 4.5 Sekunden - 5 Sekunden Nur bei WLAN verbindung streamen Nur über WLAN streamen %1$s%2$s diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index f1b3f0b5..bfa83ba4 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -322,17 +322,6 @@ Mostrar la imagen del artista en la lista de artistas Muestra la imagen del artista en la lista de artistas si está disponible Vídeo - Refresco de la vista - .5 segundos - 1 segundo - 1.5 segundos - 2 segundos - 2.5 segundos - 3 segundos - 3.5 segundos - 4 segundos - 4.5 segundos - 5 segundos Solo trasmitir medios si esta conectado a la Wi-Fi Trasmitir solo por Wi-Fi %1$s%2$s diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index 8852104f..561cceef 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -317,17 +317,6 @@ Afficher l’image de l’artiste dans la liste Affiche l’image de l’artiste dans la liste des artistes si celle-ci est disponible Vidéo - Actualisation de la vue - 0,5 secondes - 1 seconde - 1,5 secondes - 2 secondes - 2,5 secondes - 3 secondes - 3,5 secondes - 4 secondes - 4,5 secondes - 5 secondes Lire en streaming seulement si connecté en Wi-Fi Streaming en Wi-Fi uniquement %1$s%2$s diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index 5e255eed..68a6a101 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -315,17 +315,6 @@ Előadó képének megjelenítése Az előadó listában megjeleníti a képeket, amennyiben elérhetőek Videó - Nézet frissítési gyakorisága - .5 másodperc - 1 másodperc - 1.5 másodperc - 2 másodperc - 2.5 másodperc - 3 másodperc - 3.5 másodperc - 4 másodperc - 4.5 másodperc - 5 másodperc Streaming csak Wi-Fi hálózaton keresztül. Streaming csak Wi-Fivel %1$s%2$s diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index 1a0604dc..200c0ac2 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -290,16 +290,6 @@ Sfoglia Utilizzando Tag ID3 Usa metodi tag ID3 invece dei metodi basati sul filesystem Video - .5 secondo - 1 secondo - 1.5 secondi - 2 secondi - 2.5 secondi - 3 secondi - 3.5 secondi - 4 secondi - 4.5 secondi - 5 secondi %1$s%2$s %d kbps 0 B diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index f3ac5d47..c9b4970e 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -322,17 +322,6 @@ Artiestfoto tonen op artiestenlijst Toont de artiestfoto op de artiestenlijst (indien beschikbaar) Video - Verversen - 0,5 seconden - 1 seconde - 1,5 seconden - 2 seconden - 2,5 seconden - 3 seconden - 3,5 seconden - 4 seconden - 4,5 seconden - 5 seconden Alleen streamen via wifi-verbindingen Alleen streamen via wifi %1$s%2$s diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 65642da7..7985499f 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -298,17 +298,6 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników Przeglądaj używając tagów ID3 Używa metod z tagów ID3 zamiast metod opartych na systemie plików Wideo - Odświeżanie widoku - co pół sekundy - co 1 sekundę - co 1,5 sekundy - co 2 sekundy - co 2,5 sekundy - co 3 sekundy - co 3,5 sekundy - co 4 sekundy - co 4,5 sekundy - co 5 sekund Przesyłanie mediów tylko gdy Wi-fi jest włączone Przesyłanie tylko przez Wi-fi %1$s%2$s diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index 59eec69d..e39b109f 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -319,17 +319,6 @@ Mostrar Foto do Artista na Lista Mostrar a imagem do artista na lista de artistas, se disponível Vídeo - Atualização da Tela - .5 segundos - 1 segundo - 1.5 segundos - 2 segundos - 2.5 segundos - 3 segundos - 3.5 segundos - 4 segundos - 4.5 segundos - 5 segundos Somente fazer stream de mídia se conectado por Wi-Fi Streaming Somente por Wi-Fi %1$s%2$s diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index e297e3d2..13217978 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -298,17 +298,6 @@ Navegar Usando Etiquetas ID3 Usa as etiquetas ID3 ao invés do sistema de ficheiros Vídeo - Atualização do Ecrã - .5 segundos - 1 segundo - 1.5 segundos - 2 segundos - 2.5 segundos - 3 segundos - 3.5 segundos - 4 segundos - 4.5 segundos - 5 segundos Somente fazer stream de mídia se conectado por Wi-Fi Streaming Somente por Wi-Fi %1$s%2$s diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 1e806a57..39b7cbae 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -317,17 +317,6 @@ Показать изображение исполнителя в списке исполнителей Отображает изображение исполнителя в списке исполнителей, если доступно Видео - Посмотреть Обновить - .5 секунд - 1 секунда - 1.5 секунды - 2 секунды - 2.5 секунды - 3 секунды - 3.5 секунды - 4 секунды - 4.5 секунды - 5 секунд Потоковое мультимедиа только при подключении к Wi-Fi Только потоковая передача по Wi-Fi %1$s%2$s diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 07027140..fc9690b9 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -315,17 +315,6 @@ 在艺术家列表中显示艺术家图片 如果可用,在艺术家列表中显示艺术家图片 视频 - 刷新视图 - .5 秒 - 1 秒 - 1.5 秒 - 2 秒 - 2.5 秒 - 3 秒 - 3.5 秒 - 4 秒 - 4.5 秒 - 5 秒 仅在连接到 WIFI 时使用流媒体 仅使用 WIFI %1$s%2$s diff --git a/ultrasonic/src/main/res/values/arrays.xml b/ultrasonic/src/main/res/values/arrays.xml index 22b3cdff..3ea71240 100644 --- a/ultrasonic/src/main/res/values/arrays.xml +++ b/ultrasonic/src/main/res/values/arrays.xml @@ -224,30 +224,6 @@ @string/settings.search_250 @string/settings.search_500 - - @string/settings.view_refresh_500 - @string/settings.view_refresh_1000 - @string/settings.view_refresh_1500 - @string/settings.view_refresh_2000 - @string/settings.view_refresh_2500 - @string/settings.view_refresh_3000 - @string/settings.view_refresh_3500 - @string/settings.view_refresh_4000 - @string/settings.view_refresh_4500 - @string/settings.view_refresh_5000 - - - 500 - 1000 - 1500 - 2000 - 2500 - 3000 - 3500 - 4000 - 4500 - 5000 - @string/settings.share_milliseconds @string/settings.share_seconds diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 85b4b618..fe6010d8 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -328,17 +328,6 @@ Show artist picture in artist list Displays the artist picture in the artist list if available Video - View Refresh - .5 seconds - 1 second - 1.5 seconds - 2 seconds - 2.5 seconds - 3 seconds - 3.5 seconds - 4 seconds - 4.5 seconds - 5 seconds Only download media on unmetered connections Download on Wi-Fi only %1$s%2$s diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index d307bd57..57ab0815 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -48,13 +48,6 @@ a:summary="@string/settings.disc_sort_summary" a:title="@string/settings.disc_sort" app:iconSpaceReserved="false"/> - Date: Thu, 25 Nov 2021 19:44:16 +0100 Subject: [PATCH 13/33] Implement singular selection for Bookmarks --- ultrasonic/build.gradle | 1 - .../moire/ultrasonic/adapters/BaseAdapter.kt | 48 +++++++++++----- .../ultrasonic/adapters/DividerBinder.kt | 52 +++++++++++++++++ .../ultrasonic/adapters/TrackViewHolder.kt | 2 +- .../ultrasonic/fragment/BookmarksFragment.kt | 20 ++++--- .../ultrasonic/fragment/SearchFragment.kt | 18 ++++-- .../fragment/TrackCollectionFragment.kt | 10 ++-- .../moire/ultrasonic/util/BoundedTreeSet.kt | 57 +++++++++++++++++++ .../src/main/res/layout/row_divider.xml | 20 +++++++ 9 files changed, 195 insertions(+), 33 deletions(-) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt rename ultrasonic/src/main/{java => kotlin}/org/moire/ultrasonic/fragment/SearchFragment.kt (97%) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/BoundedTreeSet.kt create mode 100644 ultrasonic/src/main/res/layout/row_divider.xml diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 6bb8204e..dedf65e2 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -108,7 +108,6 @@ dependencies { implementation other.rxJava implementation other.rxAndroid implementation other.multiType - implementation 'androidx.recyclerview:recyclerview-selection:1.1.0' kapt androidSupport.room diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index 804cb308..3f91a6d5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -10,10 +10,18 @@ import androidx.recyclerview.widget.DiffUtil import com.drakeet.multitype.MultiTypeAdapter import java.util.TreeSet import org.moire.ultrasonic.domain.Identifiable +import org.moire.ultrasonic.util.BoundedTreeSet class BaseAdapter : MultiTypeAdapter() { - internal var selectedSet: TreeSet = TreeSet() + // Update the BoundedTreeSet if selection type is changed + internal var selectionType: SelectionType = SelectionType.MULTIPLE + set(newValue) { + field = newValue + selectedSet.setMaxSize(newValue.size) + } + + internal var selectedSet: BoundedTreeSet = BoundedTreeSet(selectionType.size) internal var selectionRevision: MutableLiveData = MutableLiveData(0) private val diffCallback = GenericDiffCallback() @@ -26,6 +34,7 @@ class BaseAdapter : MultiTypeAdapter() { return getItem(position).longId } + private fun getItem(position: Int): T { return mDiffer.currentList[position] } @@ -183,22 +192,33 @@ class BaseAdapter : MultiTypeAdapter() { list.add(to - 1, fromLocation) } submitList(list) - return list as List + return list } - companion object { - /** - * Calculates the differences between data sets - */ - class GenericDiffCallback : DiffUtil.ItemCallback() { - @SuppressLint("DiffUtilEquals") - override fun areContentsTheSame(oldItem: T, newItem: T): Boolean { - return oldItem == newItem - } + fun hasSingleSelection(): Boolean { + return selectionType == SelectionType.SINGLE + } - override fun areItemsTheSame(oldItem: T, newItem: T): Boolean { - return oldItem.id == newItem.id - } + fun hasMultipleSelection(): Boolean { + return selectionType == SelectionType.MULTIPLE + } + + enum class SelectionType(val size: Int) { + SINGLE(1), + MULTIPLE(Int.MAX_VALUE) + } + + /** + * Calculates the differences between data sets + */ + class GenericDiffCallback : DiffUtil.ItemCallback() { + @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 } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt new file mode 100644 index 00000000..3228ee5a --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt @@ -0,0 +1,52 @@ +package org.moire.ultrasonic.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.drakeet.multitype.ItemViewBinder +import org.moire.ultrasonic.R +import org.moire.ultrasonic.domain.Identifiable + + +/** + * Creates a row in a RecyclerView which can be used as a divide between different sections + */ +class DividerBinder: ItemViewBinder() { + + + // Set our layout files + val layout = R.layout.row_divider + + override fun onBindViewHolder(holder: ViewHolder, item: Divider) { + // Set text + holder.textView.setText(item.stringId) + } + + override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup + ): ViewHolder { + return ViewHolder(inflater.inflate(layout, parent, false)) + } + + // ViewHolder class + class ViewHolder( + itemView: View + ) : RecyclerView.ViewHolder(itemView) { + var textView: TextView = itemView.findViewById(R.id.text) + } + + // Class to store our data into + data class Divider(val stringId: Int): Identifiable { + override val id: String + get() = stringId.toString() + override val longId: Long + get() = stringId.toLong() + + override fun compareTo(other: Identifiable): Int = longId.compareTo(other.longId) + } + + +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 000b8614..06eeb313 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -276,7 +276,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable override fun setChecked(newStatus: Boolean) { observableChecked.postValue(newStatus) - check.isChecked = newStatus + //check.isChecked = newStatus } override fun isChecked(): Boolean { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt index e5431d35..5cc077cb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -7,26 +7,36 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.moire.ultrasonic.R +import org.moire.ultrasonic.adapters.BaseAdapter import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle /** * Lists the Bookmarks available on the server + * + * Bookmarks allows to save the play position of tracks, especially useful for longer tracks like + * audio books etc. + * + * Therefore this fragment allows only for singular selection and playback. + * + * // FIXME: use restore for playback */ class BookmarksFragment : TrackCollectionFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setTitle(this, R.string.button_bar_bookmarks) + + viewAdapter.selectionType = BaseAdapter.SelectionType.SINGLE } override fun setupButtons(view: View) { super.setupButtons(view) - // Why? - selectButton?.visibility = View.GONE - moreButton?.visibility = View.GONE + // Hide select all button + //selectButton?.visibility = View.GONE + //moreButton?.visibility = View.GONE } override fun getLiveData(args: Bundle?): LiveData> { @@ -37,10 +47,6 @@ class BookmarksFragment : TrackCollectionFragment() { } return listModel.currentList } - - override fun enableButtons(selection: List) { - super.enableButtons(selection) - } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt similarity index 97% rename from ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 183f7410..76002c05 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -25,6 +25,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.ArtistRowBinder +import org.moire.ultrasonic.adapters.DividerBinder import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory @@ -36,6 +37,7 @@ import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo import org.moire.ultrasonic.util.CancellationToken +import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util.toast @@ -147,6 +149,10 @@ class SearchFragment : MultiListFragment(), KoinComponent { ) ) + viewAdapter.register( + DividerBinder() + ) + // Fragment was started with a query (e.g. from voice search), try to execute search right away val arguments = arguments if (arguments != null) { @@ -415,10 +421,10 @@ class SearchFragment : MultiListFragment(), KoinComponent { } private fun search(query: String, autoplay: Boolean) { - // FIXME add error handler // FIXME support autoplay - listModel.viewModelScope.launch { + listModel.viewModelScope.launch(CommunicationError.getHandler(context)) { listModel.search(query) + } } @@ -429,7 +435,8 @@ class SearchFragment : MultiListFragment(), KoinComponent { val artists = searchResult.artists if (artists.isNotEmpty()) { - // FIXME: addView(albumsHeading) + + list.add(DividerBinder.Divider(R.string.search_artists)) list.addAll(artists) if (artists.size > DEFAULT_ARTISTS) { // FIXME @@ -438,7 +445,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { } val albums = searchResult.albums if (albums.isNotEmpty()) { - // mergeAdapter!!.addView(albumsHeading) + list.add(DividerBinder.Divider(R.string.search_albums)) list.addAll(albums) // mergeAdapter!!.addAdapter(albumAdapter) // if (albums.size > DEFAULT_ALBUMS) { @@ -447,8 +454,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { } val songs = searchResult.songs if (songs.isNotEmpty()) { -// mergeAdapter!!.addView(songsHeading) - + list.add(DividerBinder.Divider(R.string.search_albums)) list.addAll(songs) // if (songs.size > DEFAULT_SONGS) { // moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index f525fed2..5c9687f4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -30,6 +30,7 @@ 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.BaseAdapter import org.moire.ultrasonic.adapters.HeaderViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline @@ -431,6 +432,7 @@ open class TrackCollectionFragment : MultiListFragment() { val enabled = selection.isNotEmpty() var unpinEnabled = false var deleteEnabled = false + val multipleSelection = viewAdapter.hasMultipleSelection() var pinnedCount = 0 @@ -446,8 +448,8 @@ open class TrackCollectionFragment : MultiListFragment() { } playNowButton?.isVisible = enabled - playNextButton?.isVisible = enabled - playLastButton?.isVisible = enabled + playNextButton?.isVisible = enabled && multipleSelection + playLastButton?.isVisible = enabled && multipleSelection pinButton?.isVisible = (enabled && !isOffline() && selection.size > pinnedCount) unpinButton?.isVisible = (enabled && unpinEnabled) downloadButton?.isVisible = (enabled && !deleteEnabled && !isOffline()) @@ -562,8 +564,8 @@ open class TrackCollectionFragment : MultiListFragment() { val listSize = arguments?.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) ?: 0 - // Hide select button for video lists - selectButton!!.isVisible = !allVideos + // Hide select button for video lists and singular selection lists + selectButton!!.isVisible = (!allVideos && viewAdapter.hasMultipleSelection()) if (songCount > 0) { if (listSize == 0 || songCount < listSize) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/BoundedTreeSet.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/BoundedTreeSet.kt new file mode 100644 index 00000000..91bb0b22 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/BoundedTreeSet.kt @@ -0,0 +1,57 @@ +package org.moire.ultrasonic.util + +import java.util.Comparator +import java.util.SortedSet +import java.util.TreeSet + +/** + * A TreeSet that ensures it never grows beyond a max size. + * `last()` is removed if the `size()` + * get's bigger then `getMaxSize()` + */ +class BoundedTreeSet : TreeSet { + private var maxSize = Int.MAX_VALUE + + constructor(maxSize: Int) : super() { + setMaxSize(maxSize) + } + + constructor(maxSize: Int, c: Collection?) : super(c) { + setMaxSize(maxSize) + } + + constructor(maxSize: Int, c: Comparator?) : super(c) { + setMaxSize(maxSize) + } + + constructor(maxSize: Int, s: SortedSet?) : super(s) { + setMaxSize(maxSize) + } + + fun getMaxSize(): Int { + return maxSize + } + + fun setMaxSize(max: Int) { + maxSize = max + adjust() + } + + private fun adjust() { + while (maxSize < size) { + remove(last()) + } + } + + override fun add(element: E): Boolean { + val out = super.add(element) + adjust() + return out + } + + override fun addAll(elements: Collection): Boolean { + val out = super.addAll(elements) + adjust() + return out + } +} diff --git a/ultrasonic/src/main/res/layout/row_divider.xml b/ultrasonic/src/main/res/layout/row_divider.xml new file mode 100644 index 00000000..6f931fa2 --- /dev/null +++ b/ultrasonic/src/main/res/layout/row_divider.xml @@ -0,0 +1,20 @@ + + + + + + + From 4e37a2483c11dcb3fc3f397c432073d5a57e4942 Mon Sep 17 00:00:00 2001 From: tzugen Date: Fri, 26 Nov 2021 17:03:33 +0100 Subject: [PATCH 14/33] Add an MusicDirectory.Album model to represent the APIAlbum model It became necessary in order to have different types for Tracks vs Albums, instead of just differentiating by isDirectory: Boolean. Also: * Fix Album display in SearchFragment.kt * Use same ids in all lists --- .../moire/ultrasonic/domain/MusicDirectory.kt | 120 +++++++++++++----- .../moire/ultrasonic/domain/SearchResult.kt | 3 +- .../api/subsonic/models/SearchTwoResult.kt | 2 +- .../ultrasonic/util/ShufflePlayBuffer.java | 4 +- .../ultrasonic/adapters/AlbumRowBinder.kt | 12 +- .../ultrasonic/adapters/ArtistRowBinder.kt | 3 + .../moire/ultrasonic/adapters/BaseAdapter.kt | 17 ++- .../ultrasonic/adapters/DividerBinder.kt | 10 +- .../moire/ultrasonic/adapters/ImageHelper.kt | 4 +- .../ultrasonic/adapters/TrackViewHolder.kt | 3 +- .../ultrasonic/domain/APIAlbumConverter.kt | 5 +- .../ultrasonic/domain/APIArtistConverter.kt | 4 + .../ultrasonic/fragment/AlbumListFragment.kt | 16 +-- .../ultrasonic/fragment/ArtistListFragment.kt | 16 +-- .../ultrasonic/fragment/BookmarksFragment.kt | 57 ++++++--- .../ultrasonic/fragment/DownloadsFragment.kt | 18 ++- .../ultrasonic/fragment/MultiListFragment.kt | 29 +++-- .../ultrasonic/fragment/SearchFragment.kt | 71 +++++++---- .../fragment/TrackCollectionFragment.kt | 15 +-- .../ultrasonic/imageloader/ImageLoader.kt | 2 +- .../moire/ultrasonic/model/AlbumListModel.kt | 39 ++---- .../ultrasonic/model/TrackCollectionModel.kt | 32 +---- .../service/AutoMediaBrowserService.kt | 42 +++--- .../ultrasonic/service/CachedMusicService.kt | 29 +++-- .../moire/ultrasonic/service/MusicService.kt | 3 +- .../ultrasonic/service/OfflineMusicService.kt | 22 ++-- .../ultrasonic/service/RESTMusicService.kt | 4 +- .../ultrasonic/subsonic/DownloadHandler.kt | 13 +- .../moire/ultrasonic/util/DragSortCallback.kt | 1 - .../org/moire/ultrasonic/util/FileUtil.kt | 6 +- .../src/main/res/layout/generic_list.xml | 26 +--- .../src/main/res/layout/recycler_view.xml | 35 +++++ ultrasonic/src/main/res/layout/search.xml | 25 +++- .../src/main/res/layout/search_buttons.xml | 45 ------- ultrasonic/src/main/res/layout/track_list.xml | 35 +---- .../domain/APIArtistConverterTest.kt | 2 +- .../domain/APIMusicDirectoryConverterTest.kt | 4 +- .../domain/APIPlaylistConverterTest.kt | 6 +- 38 files changed, 391 insertions(+), 389 deletions(-) create mode 100644 ultrasonic/src/main/res/layout/recycler_view.xml diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt index a9c80a5c..49d02a4d 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt @@ -5,71 +5,99 @@ import androidx.room.PrimaryKey import java.io.Serializable import java.util.Date -class MusicDirectory { +class MusicDirectory : ArrayList() { var name: String? = null - private val children = mutableListOf() - fun addAll(entries: Collection) { - children.addAll(entries) + fun addFirst(child: Child) { + add(0, child) } - fun addFirst(child: Entry) { - children.add(0, child) + fun addChild(child: Child) { + add(child) } - fun addChild(child: Entry) { - children.add(child) - } - - fun findChild(id: String): Entry? = children.lastOrNull { it.id == id } - - fun getAllChild(): List = children.toList() + fun findChild(id: String): GenericEntry? = lastOrNull { it.id == id } @JvmOverloads fun getChildren( includeDirs: Boolean = true, includeFiles: Boolean = true - ): List { + ): List { if (includeDirs && includeFiles) { - return children + return toList() } - return children.filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles } + return filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles } } + fun getTracks(): List { + return mapNotNull { + it as? Entry + } + } + + fun getAlbums(): List { + return mapNotNull { + it as? Album + } + } + + abstract class Child : Identifiable, GenericEntry() { + abstract override var id: String + abstract val parent: String? + abstract val isDirectory: Boolean + abstract var album: String? + abstract val title: String? + abstract override val name: String? + abstract val discNumber: Int? + abstract val coverArt: String? + abstract val songCount: Long? + abstract val created: Date? + abstract var artist: String? + abstract val artistId: String? + abstract val duration: Int? + abstract val year: Int? + abstract val genre: String? + abstract var starred: Boolean + abstract val path: String? + abstract var closeness: Int + } + + // TODO: Rename to Track @Entity data class Entry( @PrimaryKey override var id: String, - var parent: String? = null, - var isDirectory: Boolean = false, - var title: String? = null, - var album: String? = null, + override var parent: String? = null, + override var isDirectory: Boolean = false, + override var title: String? = null, + override var album: String? = null, var albumId: String? = null, - var artist: String? = null, - var artistId: String? = null, - var track: Int? = 0, - var year: Int? = 0, - var genre: String? = null, + override var artist: String? = null, + override var artistId: String? = null, + var track: Int? = null, + override var year: Int? = null, + override var genre: String? = null, var contentType: String? = null, var suffix: String? = null, var transcodedContentType: String? = null, var transcodedSuffix: String? = null, - var coverArt: String? = null, + override var coverArt: String? = null, var size: Long? = null, - var songCount: Long? = null, - var duration: Int? = null, + override var songCount: Long? = null, + override var duration: Int? = null, var bitRate: Int? = null, - var path: String? = null, + override var path: String? = null, var isVideo: Boolean = false, - var starred: Boolean = false, - var discNumber: Int? = null, + override var starred: Boolean = false, + override var discNumber: Int? = null, var type: String? = null, - var created: Date? = null, - var closeness: Int = 0, + override var created: Date? = null, + override var closeness: Int = 0, var bookmarkPosition: Int = 0, var userRating: Int? = null, - var averageRating: Float? = null - ) : Serializable, GenericEntry() { + var averageRating: Float? = null, + override var name: String? = null + ) : Serializable, Child() { fun setDuration(duration: Long) { this.duration = duration.toInt() } @@ -94,4 +122,26 @@ class MusicDirectory { override fun compareTo(other: Identifiable) = compareTo(other as Entry) } + + data class Album( + @PrimaryKey override var id: String, + override val parent: String? = null, + override var album: String? = null, + override val title: String? = null, + override val name: String? = null, + override val discNumber: Int = 0, + override val coverArt: String? = null, + override val songCount: Long? = null, + override val created: Date? = null, + override var artist: String? = null, + override val artistId: String? = null, + override val duration: Int = 0, + override val year: Int = 0, + override val genre: String? = null, + override var starred: Boolean = false, + override var path: String? = null, + override var closeness: Int = 0, + ) : Child() { + override val isDirectory = true + } } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt index 11c4c97c..7c8ee9bd 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt @@ -1,5 +1,6 @@ package org.moire.ultrasonic.domain +import org.moire.ultrasonic.domain.MusicDirectory.Album import org.moire.ultrasonic.domain.MusicDirectory.Entry /** @@ -7,6 +8,6 @@ import org.moire.ultrasonic.domain.MusicDirectory.Entry */ data class SearchResult( val artists: List = listOf(), - val albums: List = listOf(), + val albums: List = listOf(), val songs: List = listOf() ) diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchTwoResult.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchTwoResult.kt index 1b42f640..5e94c22a 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchTwoResult.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchTwoResult.kt @@ -4,6 +4,6 @@ import com.fasterxml.jackson.annotation.JsonProperty data class SearchTwoResult( @JsonProperty("artist") val artistList: List = emptyList(), - @JsonProperty("album") val albumList: List = emptyList(), + @JsonProperty("album") val albumList: List = emptyList(), @JsonProperty("song") val songList: List = emptyList() ) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java index d918a4e0..156ea868 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java @@ -100,8 +100,8 @@ public class ShufflePlayBuffer synchronized (buffer) { - buffer.addAll(songs.getChildren()); - Timber.i("Refilled shuffle play buffer with %d songs.", songs.getChildren().size()); + buffer.addAll(songs.getTracks()); + Timber.i("Refilled shuffle play buffer with %d songs.", songs.getTracks().size()); } } catch (Exception x) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt index df0fa70e..c7ce06d0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt @@ -31,11 +31,11 @@ import timber.log.Timber * Creates a Row in a RecyclerView which contains the details of an Album */ class AlbumRowBinder( - val onItemClick: (MusicDirectory.Entry) -> Unit, - val onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean, + val onItemClick: (MusicDirectory.Album) -> Unit, + val onContextMenuClick: (MenuItem, MusicDirectory.Album) -> Boolean, private val imageLoader: ImageLoader, - context: Context, -) : ItemViewBinder(), KoinComponent { + context: Context +) : ItemViewBinder(), KoinComponent { private val starDrawable: Drawable = Util.getDrawableFromAttribute(context, R.attr.star_full) @@ -46,7 +46,7 @@ class AlbumRowBinder( val layout = R.layout.album_list_item val contextMenuLayout = R.menu.artist_context_menu - override fun onBindViewHolder(holder: ViewHolder, item: MusicDirectory.Entry) { + override fun onBindViewHolder(holder: ViewHolder, item: MusicDirectory.Album) { holder.album.text = item.title holder.artist.text = item.artist holder.details.setOnClickListener { onItemClick(item) } @@ -86,7 +86,7 @@ class AlbumRowBinder( /** * Handles the star / unstar action for an album */ - private fun onStarClick(entry: MusicDirectory.Entry, star: ImageView) { + private fun onStarClick(entry: MusicDirectory.Album, star: ImageView) { entry.starred = !entry.starred star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable) val musicService = getMusicService() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt index f850e037..41a2e7a5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt @@ -14,6 +14,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.RelativeLayout import android.widget.TextView +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.drakeet.multitype.ItemViewBinder import org.koin.core.component.KoinComponent @@ -31,6 +32,7 @@ class ArtistRowBinder( val onItemClick: (ArtistOrIndex) -> Unit, val onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean, private val imageLoader: ImageLoader, + private val enableSections: Boolean = true ) : ItemViewBinder(), KoinComponent { val layout = R.layout.artist_list_item @@ -39,6 +41,7 @@ class ArtistRowBinder( override fun onBindViewHolder(holder: ViewHolder, item: ArtistOrIndex) { holder.textView.text = item.name holder.section.text = getSectionForArtist(item) + holder.section.isVisible = enableSections holder.layout.setOnClickListener { onItemClick(item) } holder.layout.setOnLongClickListener { val popup = Helper.createPopupMenu(holder.itemView) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index 3f91a6d5..6e53062a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -1,3 +1,10 @@ +/* + * BaseAdapter.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.adapters import android.annotation.SuppressLint @@ -8,10 +15,15 @@ import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer.ListListener import androidx.recyclerview.widget.DiffUtil import com.drakeet.multitype.MultiTypeAdapter -import java.util.TreeSet import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.util.BoundedTreeSet +/** + * The BaseAdapter which extends the MultiTypeAdapter from an external library. + * It provides selection support as well as Diffing the submitted lists for performance. + * + * It should be kept generic enought that it can be used a Base for all lists in the app. + */ class BaseAdapter : MultiTypeAdapter() { // Update the BoundedTreeSet if selection type is changed @@ -34,11 +46,12 @@ class BaseAdapter : MultiTypeAdapter() { return getItem(position).longId } - private fun getItem(position: Int): T { return mDiffer.currentList[position] } + // override getIt + override var items: List get() = getCurrentList() set(value) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt index 3228ee5a..c8dcf395 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt @@ -9,12 +9,10 @@ import com.drakeet.multitype.ItemViewBinder import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.Identifiable - /** * Creates a row in a RecyclerView which can be used as a divide between different sections */ -class DividerBinder: ItemViewBinder() { - +class DividerBinder : ItemViewBinder() { // Set our layout files val layout = R.layout.row_divider @@ -39,7 +37,7 @@ class DividerBinder: ItemViewBinder.toDomainEntityList(): List = this.map { it.toDomainEntity() } +fun List.toDomainEntityList(): List = this.map { it.toDomainEntity() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt index 51c2c72f..4c2294ba 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt @@ -23,3 +23,7 @@ fun APIArtist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory(). name = this@toMusicDirectoryDomainEntity.name addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity() }) } + +fun APIArtist.toDomainEntityList(): List { + return this.albumsList.map { it.toDomainEntity() } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index cc11a99b..49df1d34 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -16,7 +16,7 @@ import org.moire.ultrasonic.util.Constants * Displays a list of Albums from the media library * FIXME: Add music folder support */ -class AlbumListFragment : EntryListFragment() { +class AlbumListFragment : EntryListFragment() { /** * The ViewModel to use to get the data @@ -28,16 +28,6 @@ class AlbumListFragment : EntryListFragment() { */ 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, * after the user has clicked on an item @@ -47,7 +37,7 @@ class AlbumListFragment : EntryListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(args: Bundle?): LiveData> { + override fun getLiveData(args: Bundle?): LiveData> { if (args == null) throw IllegalArgumentException("Required arguments are missing") val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) @@ -83,7 +73,7 @@ class AlbumListFragment : EntryListFragment() { ) } - override fun onItemClick(item: MusicDirectory.Entry) { + override fun onItemClick(item: MusicDirectory.Album) { val bundle = Bundle() bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id) bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, item.isDirectory) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 10b9c7ee..d3169e1e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -27,16 +27,6 @@ class ArtistListFragment : EntryListFragment() { */ override val mainLayout = R.layout.generic_list - /** - * The id of the refresh view - */ - override val refreshListId = 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 @@ -69,8 +59,10 @@ class ArtistListFragment : EntryListFragment() { 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)) - bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, - Constants.ALPHABETICAL_BY_NAME) + bundle.putString( + Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, + Constants.ALPHABETICAL_BY_NAME + ) bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt index 5cc077cb..54dfcccf 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -1,14 +1,19 @@ +/* + * BookmarksFragment.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.fragment import android.os.Bundle import android.view.View -import androidx.core.view.isVisible import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.BaseAdapter -import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle @@ -20,7 +25,7 @@ import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle * * Therefore this fragment allows only for singular selection and playback. * - * // FIXME: use restore for playback + * FIXME: use restore for playback */ class BookmarksFragment : TrackCollectionFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -31,14 +36,6 @@ class BookmarksFragment : TrackCollectionFragment() { viewAdapter.selectionType = BaseAdapter.SelectionType.SINGLE } - override fun setupButtons(view: View) { - super.setupButtons(view) - - // Hide select all button - //selectButton?.visibility = View.GONE - //moreButton?.visibility = View.GONE - } - override fun getLiveData(args: Bundle?): LiveData> { listModel.viewModelScope.launch(handler) { refreshListView?.isRefreshing = true @@ -47,12 +44,34 @@ class BookmarksFragment : TrackCollectionFragment() { } return listModel.currentList } + + /** + * Set a custom listener to perform the playing, in order to be able to restore + * the playback position + */ + override fun setupButtons(view: View) { + super.setupButtons(view) + + playNowButton!!.setOnClickListener { + playNow(getSelectedSongs()) + } + } + + /** + * Custom playback function which uses the restore functionality. A bit of a hack.. + */ + private fun playNow(songs: List) { + if (songs.isNotEmpty()) { + + val position = songs[0].bookmarkPosition + + mediaPlayerController.restore( + songs = songs, + currentPlayingIndex = 0, + currentPlayingPosition = position, + autoPlay = true, + newPlaylist = true + ) + } + } } - - - - - - - - diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index 86dc63af..70c70986 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -1,3 +1,10 @@ +/* + * DownloadsFragment.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.fragment import android.app.Application @@ -14,6 +21,13 @@ import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.Downloader import org.moire.ultrasonic.util.Util +/** + * Displays currently running downloads. + * For now its a read-only view, there are no manipulations of the download list possible. + * + * A consideration would be to base this class on TrackCollectionFragment and thereby inheriting the + * buttons useful to manipulate the list. + */ class DownloadsFragment : MultiListFragment() { /** @@ -60,7 +74,9 @@ class DownloadsFragment : MultiListFragment() { ) ) - viewAdapter.submitList(listModel.getList().value) + val liveDataList = listModel.getList() + + viewAdapter.submitList(liveDataList.value) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index 8d6725a7..635f7632 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -1,3 +1,10 @@ +/* + * MultiListFragment.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.fragment import android.os.Bundle @@ -5,6 +12,8 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData @@ -37,6 +46,7 @@ abstract class MultiListFragment : Fragment() { protected var refreshListView: SwipeRefreshLayout? = null internal var listView: RecyclerView? = null internal lateinit var viewManager: LinearLayoutManager + internal lateinit var emptyTextView: TextView /** * The Adapter for the RecyclerView @@ -76,14 +86,11 @@ abstract class MultiListFragment : Fragment() { open val mainLayout: Int = R.layout.generic_list /** - * The id of the refresh view + * The ids of the swipe refresh view, the recycler view and the empty text view */ - open val refreshListId: Int = R.id.generic_list_refresh - - /** - * The id of the RecyclerView - */ - open val recyclerViewId = R.id.generic_list_recycler + open val refreshListId = R.id.swipe_refresh_view + open val recyclerViewId = R.id.recycler_view + open val emptyTextViewId = R.id.empty_list_text open fun setTitle(title: String?) { if (title == null) { @@ -113,11 +120,15 @@ abstract class MultiListFragment : Fragment() { // Populate the LiveData. This starts an API request in most cases liveDataItems = getLiveData(arguments) + // Link view to display text if the list is empty + // FIXME: Hook this up globally. + emptyTextView = view.findViewById(emptyTextViewId) + // Register an observer to update our UI when the data changes liveDataItems.observe( viewLifecycleOwner, - { - newItems -> + { newItems -> + emptyTextView.isVisible = newItems.isEmpty() viewAdapter.submitList(newItems) } ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 76002c05..19553929 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -12,8 +12,8 @@ import android.view.MenuItem import android.view.View import android.widget.AdapterView.AdapterContextMenuInfo import android.widget.ListAdapter -import android.widget.TextView import androidx.appcompat.widget.SearchView +import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -24,6 +24,7 @@ import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.R +import org.moire.ultrasonic.adapters.AlbumRowBinder import org.moire.ultrasonic.adapters.ArtistRowBinder import org.moire.ultrasonic.adapters.DividerBinder import org.moire.ultrasonic.adapters.TrackViewBinder @@ -48,10 +49,6 @@ import timber.log.Timber * Initiates a search on the media library and displays the results */ class SearchFragment : MultiListFragment(), KoinComponent { - private var artistsHeading: View? = null - private var albumsHeading: View? = null - private var songsHeading: View? = null - private var notFound: TextView? = null private var moreArtistsButton: View? = null private var moreAlbumsButton: View? = null private var moreSongsButton: View? = null @@ -71,8 +68,6 @@ class SearchFragment : MultiListFragment(), KoinComponent { override val listModel: SearchListModel by viewModels() - override val recyclerViewId = R.id.search_list - override val mainLayout: Int = R.layout.search override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -87,10 +82,6 @@ class SearchFragment : MultiListFragment(), KoinComponent { ) if (buttons != null) { - artistsHeading = buttons.findViewById(R.id.search_artists) - albumsHeading = buttons.findViewById(R.id.search_albums) - songsHeading = buttons.findViewById(R.id.search_songs) - notFound = buttons.findViewById(R.id.search_not_found) moreArtistsButton = buttons.findViewById(R.id.search_more_artists) moreAlbumsButton = buttons.findViewById(R.id.search_more_albums) moreSongsButton = buttons.findViewById(R.id.search_more_songs) @@ -103,7 +94,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { } ) - searchRefresh = view.findViewById(R.id.search_entries_refresh) + searchRefresh = view.findViewById(R.id.swipe_refresh_view) searchRefresh!!.isEnabled = false // list.setOnItemClickListener(OnItemClickListener { parent: AdapterView<*>, view1: View, position: Int, id: Long -> @@ -132,6 +123,37 @@ class SearchFragment : MultiListFragment(), KoinComponent { registerForContextMenu(listView!!) + // Register our data binders + // IMPORTANT: + // They need to be added in the order of most specific -> least specific. + viewAdapter.register( + ArtistRowBinder( + onItemClick = { entry -> onItemClick(entry) }, + onContextMenuClick = { menuItem, entry -> + onContextMenuItemSelected( + menuItem, + entry + ) + }, + imageLoader = imageLoaderProvider.getImageLoader(), + enableSections = false + ) + ) + + viewAdapter.register( + AlbumRowBinder( + onItemClick = { entry -> onItemClick(entry) }, + onContextMenuClick = { menuItem, entry -> + onContextMenuItemSelected( + menuItem, + entry + ) + }, + imageLoader = imageLoaderProvider.getImageLoader(), + context = requireContext() + ) + ) + viewAdapter.register( TrackViewBinder( checkable = false, @@ -141,14 +163,6 @@ class SearchFragment : MultiListFragment(), KoinComponent { ) ) - viewAdapter.register( - ArtistRowBinder( - { entry -> onItemClick(entry) }, - { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, - imageLoaderProvider.getImageLoader() - ) - ) - viewAdapter.register( DividerBinder() ) @@ -164,7 +178,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { } // Fragment was started from the Menu, create empty list - populateList(SearchResult()) + // populateList(SearchResult()) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -180,11 +194,13 @@ class SearchFragment : MultiListFragment(), KoinComponent { val autoPlay = arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) val query = arguments?.getString(Constants.INTENT_EXTRA_NAME_QUERY) + // If started with a query, enter it to the searchView if (query != null) { searchView.setQuery(query, false) searchView.clearFocus() } + searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener { override fun onSuggestionSelect(position: Int): Boolean { return true @@ -423,8 +439,9 @@ class SearchFragment : MultiListFragment(), KoinComponent { private fun search(query: String, autoplay: Boolean) { // FIXME support autoplay listModel.viewModelScope.launch(CommunicationError.getHandler(context)) { + refreshListView?.isRefreshing = true listModel.search(query) - + refreshListView?.isRefreshing = false } } @@ -454,17 +471,15 @@ class SearchFragment : MultiListFragment(), KoinComponent { } val songs = searchResult.songs if (songs.isNotEmpty()) { - list.add(DividerBinder.Divider(R.string.search_albums)) + list.add(DividerBinder.Divider(R.string.search_songs)) list.addAll(songs) // if (songs.size > DEFAULT_SONGS) { // moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true) // } } - // FIXME - if (list.isEmpty()) { - // mergeAdapter!!.addView(notFound, false) - } + // Show/hide the empty text view + emptyTextView.isVisible = list.isEmpty() viewAdapter.submitList(list) } @@ -506,7 +521,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { // Navigation.findNavController(requireView()).navigate(R.id.searchToSelectAlbum, bundle) // } - private fun onAlbumSelected(album: MusicDirectory.Entry, autoplay: Boolean) { + private fun onAlbumSelected(album: MusicDirectory.Album, autoplay: Boolean) { val bundle = Bundle() bundle.putString(Constants.INTENT_EXTRA_NAME_ID, album.id) bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, album.title) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 5c9687f4..6d15cc8d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -30,7 +30,6 @@ 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.BaseAdapter import org.moire.ultrasonic.adapters.HeaderViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline @@ -85,16 +84,6 @@ open class TrackCollectionFragment : MultiListFragment() { */ 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 @@ -118,7 +107,7 @@ open class TrackCollectionFragment : MultiListFragment() { setupButtons(view) - emptyView = view.findViewById(R.id.select_album_empty) + emptyView = view.findViewById(R.id.empty_list_text) registerForContextMenu(listView!!) setHasOptionsMenu(true) @@ -629,7 +618,7 @@ open class TrackCollectionFragment : MultiListFragment() { listModel.currentListIsSortable = true } - private fun getSelectedSongs(): List { + internal fun getSelectedSongs(): List { // Walk through selected set and get the Entries based on the saved ids. return viewAdapter.getCurrentList().mapNotNull { if (it is MusicDirectory.Entry && viewAdapter.isSelected(it.longId)) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index 4b28e82c..09f6dc3c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -87,7 +87,7 @@ class ImageLoader( @JvmOverloads fun loadImage( view: View?, - entry: MusicDirectory.Entry?, + entry: MusicDirectory.Child?, large: Boolean, size: Int, defaultResourceId: Int = R.drawable.unknown_album diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt index 6c28f94c..24dfd80e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -5,7 +5,6 @@ import android.os.Bundle import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.service.MusicService @@ -14,7 +13,7 @@ import org.moire.ultrasonic.util.Settings class AlbumListModel(application: Application) : GenericListModel(application) { - val list: MutableLiveData> = MutableLiveData(listOf()) + val list: MutableLiveData> = MutableLiveData(listOf()) var lastType: String? = null private var loadedUntil: Int = 0 @@ -22,7 +21,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) { refresh: Boolean, swipe: SwipeRefreshLayout, args: Bundle - ): LiveData> { + ): LiveData> { // Don't reload the data if navigating back to the view that was active before. // This way, we keep the scroll position val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!! @@ -35,29 +34,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) { } fun getAlbumsOfArtist(musicService: MusicService, refresh: Boolean, id: String, name: String?) { - - var root = MusicDirectory() - val musicDirectory = musicService.getArtist(id, name, refresh) - - if (Settings.shouldShowAllSongsByArtist && - musicDirectory.findChild(allSongsId) == null && - hasOnlyFolders(musicDirectory) - ) { - val allSongs = MusicDirectory.Entry(allSongsId) - - allSongs.isDirectory = true - allSongs.artist = name - allSongs.parent = id - allSongs.title = String.format( - context.resources.getString(R.string.select_album_all_songs), name - ) - - root.addFirst(allSongs) - root.addAll(musicDirectory.getChildren()) - } else { - root = musicDirectory - } - list.postValue(root.getChildren()) + list.postValue(musicService.getArtist(id, name, refresh)) } override fun load( @@ -108,13 +85,15 @@ class AlbumListModel(application: Application) : GenericListModel(application) { currentListIsSortable = isCollectionSortable(albumListType) + // TODO: Change signature of musicService.getAlbumList to return a List + @Suppress("UNCHECKED_CAST") if (append && list.value != null) { - val list = ArrayList() + val list = ArrayList() list.addAll(this.list.value!!) - list.addAll(musicDirectory.getAllChild()) - this.list.postValue(list) + list.addAll(musicDirectory.getChildren()) + this.list.postValue(list as List) } else { - list.postValue(musicDirectory.getAllChild()) + list.postValue(musicDirectory.getChildren() as List) } loadedUntil = offset diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index ee5b4e27..ac9ef7c1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -12,7 +12,6 @@ import androidx.lifecycle.MutableLiveData import java.util.LinkedList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.Settings @@ -54,25 +53,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } } else { val musicDirectory = service.getMusicDirectory(id, name, refresh) - - if (Settings.shouldShowAllSongsByArtist && - musicDirectory.findChild(allSongsId) == null && - hasOnlyFolders(musicDirectory) - ) { - val allSongs = MusicDirectory.Entry(allSongsId) - - allSongs.isDirectory = true - allSongs.artist = name - allSongs.parent = id - allSongs.title = String.format( - context.resources.getString(R.string.select_album_all_songs), name - ) - - root.addChild(allSongs) - root.addAll(musicDirectory.getChildren()) - } else { - root = musicDirectory - } + root = musicDirectory } currentDirectory.postValue(root) @@ -87,13 +68,13 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat ) { val service = MusicServiceFactory.getMusicService() - for (song in parent.getChildren(includeDirs = false, includeFiles = true)) { + for (song in parent.getTracks()) { if (!song.isVideo && !song.isDirectory) { songs.add(song) } } - for ((id1, _, _, title) in parent.getChildren(true, includeFiles = false)) { + for ((id1, _, _, title) in parent.getAlbums()) { var root: MusicDirectory if (allSongsId != id1) { @@ -118,13 +99,14 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat val songs: MutableCollection = LinkedList() val artist = service.getArtist(parentId, "", false) - for ((id1) in artist.getChildren()) { + // FIXME is still working? + for ((id1) in artist) { if (allSongsId != id1) { val albumDirectory = service.getAlbum( id1, "", false ) - for (song in albumDirectory.getChildren()) { + for (song in albumDirectory.getTracks()) { if (!song.isVideo) { songs.add(song) } @@ -252,6 +234,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } private fun updateList(root: MusicDirectory) { - currentList.postValue(root.getChildren()) + currentList.postValue(root.getTracks()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt index 173c5806..c44d202f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt @@ -484,10 +484,12 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val albums = if (!isOffline && useId3Tags) { callWithErrorHandling { musicService.getArtist(id, name, false) } } else { - callWithErrorHandling { musicService.getMusicDirectory(id, name, false) } + callWithErrorHandling { + musicService.getMusicDirectory(id, name, false).getAlbums() + } } - albums?.getAllChild()?.map { album -> + albums?.map { album -> mediaItems.add( album.title ?: "", listOf(MEDIA_ALBUM_ITEM, album.id, album.name) @@ -517,7 +519,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|")) // TODO: Paging is not implemented for songs, is it necessary at all? - val items = songs.getChildren().take(DISPLAY_LIMIT) + val items = songs.getTracks().take(DISPLAY_LIMIT) items.map { item -> if (item.isDirectory) mediaItems.add( @@ -573,7 +575,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - albums?.getAllChild()?.map { album -> + albums?.getChildren()?.map { album -> mediaItems.add( album.title ?: "", listOf(MEDIA_ALBUM_ITEM, album.id, album.name) @@ -582,7 +584,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { ) } - if (albums?.getAllChild()?.count() ?: 0 >= DISPLAY_LIMIT) + if (albums?.getChildren()?.count() ?: 0 >= DISPLAY_LIMIT) mediaItems.add( R.string.search_more, listOf(MEDIA_ALBUM_PAGE_ID, type.typeName, (page ?: 0) + 1).joinToString("|"), @@ -624,13 +626,13 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val content = callWithErrorHandling { musicService.getPlaylist(id, name) } if (content != null) { - if (content.getAllChild().count() > 1) + if (content.getChildren().count() > 1) mediaItems.addPlayAllItem( listOf(MEDIA_PLAYLIST_ITEM, id, name).joinToString("|") ) // Playlist should be cached as it may contain random elements - playlistCache = content.getAllChild() + playlistCache = content.getTracks() playlistCache!!.take(DISPLAY_LIMIT).map { item -> mediaItems.add( MediaBrowserCompat.MediaItem( @@ -657,7 +659,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { if (playlistCache == null) { // This can only happen if Android Auto cached items, but Ultrasonic has forgot them val content = callWithErrorHandling { musicService.getPlaylist(id, name) } - playlistCache = content?.getAllChild() + playlistCache = content?.getTracks() } if (playlistCache != null) playSongs(playlistCache) } @@ -668,7 +670,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { if (playlistCache == null) { // This can only happen if Android Auto cached items, but Ultrasonic has forgot them val content = callWithErrorHandling { musicService.getPlaylist(id, name) } - playlistCache = content?.getAllChild() + playlistCache = content?.getTracks() } val song = playlistCache?.firstOrNull { x -> x.id == songId } if (song != null) playSong(song) @@ -678,14 +680,14 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { private fun playAlbum(id: String, name: String) { serviceScope.launch { val songs = listSongsInMusicService(id, name) - if (songs != null) playSongs(songs.getAllChild()) + if (songs != null) playSongs(songs.getTracks()) } } private fun playAlbumSong(id: String, name: String, songId: String) { serviceScope.launch { val songs = listSongsInMusicService(id, name) - val song = songs?.getAllChild()?.firstOrNull { x -> x.id == songId } + val song = songs?.getTracks()?.firstOrNull { x -> x.id == songId } if (song != null) playSong(song) } } @@ -717,10 +719,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) } if (episodes != null) { - if (episodes.getAllChild().count() > 1) + if (episodes.getTracks().count() > 1) mediaItems.addPlayAllItem(listOf(MEDIA_PODCAST_ITEM, id).joinToString("|")) - episodes.getAllChild().map { episode -> + episodes.getTracks().map { episode -> mediaItems.add( MediaBrowserCompat.MediaItem( Util.getMediaDescriptionForEntry( @@ -741,7 +743,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { serviceScope.launch { val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) } if (episodes != null) { - playSongs(episodes.getAllChild()) + playSongs(episodes.getTracks()) } } } @@ -751,7 +753,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) } if (episodes != null) { val selectedEpisode = episodes - .getAllChild() + .getTracks() .firstOrNull { episode -> episode.id == episodeId } if (selectedEpisode != null) playSong(selectedEpisode) } @@ -766,7 +768,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { if (bookmarks != null) { val songs = Util.getSongsFromBookmarks(bookmarks) - songs.getAllChild().map { song -> + songs.getTracks().map { song -> mediaItems.add( MediaBrowserCompat.MediaItem( Util.getMediaDescriptionForEntry( @@ -787,7 +789,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val bookmarks = callWithErrorHandling { musicService.getBookmarks() } if (bookmarks != null) { val songs = Util.getSongsFromBookmarks(bookmarks) - val song = songs.getAllChild().firstOrNull { song -> song.id == id } + val song = songs.getTracks().firstOrNull { song -> song.id == id } if (song != null) playSong(song) } } @@ -926,11 +928,11 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val songs = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) } if (songs != null) { - if (songs.getAllChild().count() > 1) + if (songs.getChildren().count() > 1) mediaItems.addPlayAllItem(listOf(MEDIA_SONG_RANDOM_ID).joinToString("|")) // TODO: Paging is not implemented for songs, is it necessary at all? - val items = songs.getAllChild() + val items = songs.getTracks() randomSongsCache = items items.map { song -> mediaItems.add( @@ -954,7 +956,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { // This can only happen if Android Auto cached items, but Ultrasonic has forgot them // In this case we request a new set of random songs val content = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) } - randomSongsCache = content?.getAllChild() + randomSongsCache = content?.getTracks() } if (randomSongsCache != null) playSongs(randomSongsCache) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index fed88a9b..bca7aa51 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -41,7 +41,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, // Old style TimeLimitedCache private val cachedMusicDirectories: LRUCache> - private val cachedArtist: LRUCache> + private val cachedArtist: LRUCache>> private val cachedAlbum: LRUCache> private val cachedUserInfo: LRUCache> private val cachedLicenseValid = TimeLimitedCache(120, TimeUnit.SECONDS) @@ -148,20 +148,21 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, } @Throws(Exception::class) - override fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory { - checkSettingsChanged() - var cache = if (refresh) null else cachedArtist[id] - var dir = cache?.get() - if (dir == null) { - dir = musicService.getArtist(id, name, refresh) - cache = TimeLimitedCache( - Settings.directoryCacheTime.toLong(), TimeUnit.SECONDS - ) - cache.set(dir) - cachedArtist.put(id, cache) + override fun getArtist(id: String, name: String?, refresh: Boolean): + List { + checkSettingsChanged() + var cache = if (refresh) null else cachedArtist[id] + var dir = cache?.get() + if (dir == null) { + dir = musicService.getArtist(id, name, refresh) + cache = TimeLimitedCache( + Settings.directoryCacheTime.toLong(), TimeUnit.SECONDS + ) + cache.set(dir) + cachedArtist.put(id, cache) + } + return dir } - return dir - } @Throws(Exception::class) override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index 902ab3f9..5d78644f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -24,6 +24,7 @@ import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.domain.UserInfo @Suppress("TooManyFunctions") + interface MusicService { @Throws(Exception::class) fun ping() @@ -56,7 +57,7 @@ interface MusicService { fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory @Throws(Exception::class) - fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory + fun getArtist(id: String, name: String?, refresh: Boolean): List @Throws(Exception::class) fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index 9ec622b7..d1b26e5c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -14,7 +14,6 @@ import java.io.FileReader import java.io.FileWriter import java.io.InputStream import java.io.Reader -import java.lang.Math.min import java.util.ArrayList import java.util.HashSet import java.util.LinkedList @@ -119,7 +118,7 @@ class OfflineMusicService : MusicService, KoinComponent { override fun search(criteria: SearchCriteria): SearchResult { val artists: MutableList = ArrayList() - val albums: MutableList = ArrayList() + val albums: MutableList = ArrayList() val songs: MutableList = ArrayList() val root = FileUtil.musicDirectory var closeness: Int @@ -258,7 +257,7 @@ class OfflineMusicService : MusicService, KoinComponent { return result } children.shuffle() - val finalSize: Int = min(children.size, size) + val finalSize: Int = children.size.coerceAtMost(size) for (i in 0 until finalSize) { val file = children[i % children.size] result.addChild(createEntry(file, getName(file))) @@ -447,9 +446,10 @@ class OfflineMusicService : MusicService, KoinComponent { } @Throws(OfflineException::class) - override fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory { - throw OfflineException("getArtist isn't available in offline mode") - } + override fun getArtist(id: String, name: String?, refresh: Boolean): + List { + throw OfflineException("getArtist isn't available in offline mode") + } @Throws(OfflineException::class) override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory { @@ -498,7 +498,7 @@ class OfflineMusicService : MusicService, KoinComponent { } @Suppress("TooGenericExceptionCaught", "ComplexMethod", "LongMethod", "NestedBlockDepth") - private fun createEntry(file: File, name: String?): MusicDirectory.Entry { + private fun createEntry(file: File, name: String?): MusicDirectory.Child { val entry = MusicDirectory.Entry(file.path) entry.isDirectory = file.isDirectory entry.parent = file.parent @@ -600,7 +600,7 @@ class OfflineMusicService : MusicService, KoinComponent { artistName: String, file: File, criteria: SearchCriteria, - albums: MutableList, + albums: MutableList, songs: MutableList ) { var closeness: Int @@ -611,7 +611,7 @@ class OfflineMusicService : MusicService, KoinComponent { val album = createEntry(albumFile, albumName) album.artist = artistName album.closeness = closeness - albums.add(album) + albums.add(album as MusicDirectory.Album) } for (songFile in FileUtil.listMediaFiles(albumFile)) { val songName = getName(songFile) @@ -622,7 +622,7 @@ class OfflineMusicService : MusicService, KoinComponent { song.artist = artistName song.album = albumName song.closeness = closeness - songs.add(song) + songs.add(song as MusicDirectory.Entry) } } } else { @@ -632,7 +632,7 @@ class OfflineMusicService : MusicService, KoinComponent { song.artist = artistName song.album = songName song.closeness = closeness - songs.add(song) + songs.add(song as MusicDirectory.Entry) } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 3121fbe6..9ce8f92b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -143,10 +143,10 @@ open class RESTMusicService( id: String, name: String?, refresh: Boolean - ): MusicDirectory { + ): List { val response = API.getArtist(id).execute().throwOnFailure() - return response.body()!!.artist.toMusicDirectoryDomainEntity() + return response.body()!!.artist.toDomainEntityList() } @Throws(Exception::class) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index 1d14d2fb..7ae5ba08 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -240,18 +240,13 @@ class DownloadHandler( if (songs.size > maxSongs) { return } - for (song in parent.getChildren(includeDirs = false, includeFiles = true)) { + for (song in parent.getTracks()) { if (!song.isVideo) { songs.add(song) } } val musicService = getMusicService() - for ( - (id1, _, _, title) in parent.getChildren( - includeDirs = true, - includeFiles = false - ) - ) { + for ((id1, _, _, title) in parent.getAlbums()) { val root: MusicDirectory = if ( !isOffline() && Settings.shouldUseId3Tags @@ -271,13 +266,13 @@ class DownloadHandler( } val musicService = getMusicService() val artist = musicService.getArtist(id, "", false) - for ((id1) in artist.getChildren()) { + for ((id1) in artist) { val albumDirectory = musicService.getAlbum( id1, "", false ) - for (song in albumDirectory.getChildren()) { + for (song in albumDirectory.getTracks()) { if (!song.isVideo) { songs.add(song) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt index 91894874..f090d9d5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt @@ -5,7 +5,6 @@ import androidx.recyclerview.widget.ItemTouchHelper.DOWN import androidx.recyclerview.widget.ItemTouchHelper.UP import androidx.recyclerview.widget.RecyclerView import org.moire.ultrasonic.adapters.BaseAdapter -import timber.log.Timber class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt index f9a03051..902ad61f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt @@ -119,7 +119,7 @@ object FileUtil { * @param large Whether to get the key for the large or the default image * @return String The hash key */ - fun getAlbumArtKey(entry: MusicDirectory.Entry?, large: Boolean): String? { + fun getAlbumArtKey(entry: MusicDirectory.Child?, large: Boolean): String? { if (entry == null) return null val albumDir = getAlbumDirectory(entry) return getAlbumArtKey(albumDir, large) @@ -190,7 +190,7 @@ object FileUtil { return albumArtDir } - fun getAlbumDirectory(entry: MusicDirectory.Entry): File { + fun getAlbumDirectory(entry: MusicDirectory.Child): File { val dir: File if (!TextUtils.isEmpty(entry.path)) { val f = File(fileSystemSafeDir(entry.path)) @@ -457,7 +457,7 @@ object FileUtil { try { fw.write("#EXTM3U\n") - for (e in playlist.getChildren()) { + for (e in playlist.getTracks()) { var filePath = getSongFile(e).absolutePath if (!File(filePath).exists()) { diff --git a/ultrasonic/src/main/res/layout/generic_list.xml b/ultrasonic/src/main/res/layout/generic_list.xml index 1cb9529d..45bce488 100644 --- a/ultrasonic/src/main/res/layout/generic_list.xml +++ b/ultrasonic/src/main/res/layout/generic_list.xml @@ -2,32 +2,8 @@ - - - - - + diff --git a/ultrasonic/src/main/res/layout/recycler_view.xml b/ultrasonic/src/main/res/layout/recycler_view.xml new file mode 100644 index 00000000..09ca5ccc --- /dev/null +++ b/ultrasonic/src/main/res/layout/recycler_view.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/search.xml b/ultrasonic/src/main/res/layout/search.xml index a5132493..391d278f 100644 --- a/ultrasonic/src/main/res/layout/search.xml +++ b/ultrasonic/src/main/res/layout/search.xml @@ -1,20 +1,31 @@ + a:layout_width="fill_parent" + a:layout_height="fill_parent" + a:orientation="vertical"> + + - + a:layout_weight="1.0" /> diff --git a/ultrasonic/src/main/res/layout/search_buttons.xml b/ultrasonic/src/main/res/layout/search_buttons.xml index 66b82755..1666bdd1 100644 --- a/ultrasonic/src/main/res/layout/search_buttons.xml +++ b/ultrasonic/src/main/res/layout/search_buttons.xml @@ -4,51 +4,6 @@ a:layout_width="fill_parent" a:layout_height="wrap_content"> - - - - - - - - - - - - - - - + diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIArtistConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIArtistConverterTest.kt index 37c8f5c9..4ae7528b 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIArtistConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIArtistConverterTest.kt @@ -42,7 +42,7 @@ class APIArtistConverterTest { with(convertedEntity) { name `should be equal to` entity.name - getAllChild() `should be equal to` entity.albumsList + getChildren() `should be equal to` entity.albumsList .map { it.toDomainEntity() }.toMutableList() } } diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverterTest.kt index e03da15d..938d1807 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverterTest.kt @@ -24,8 +24,8 @@ class APIMusicDirectoryConverterTest { with(convertedEntity) { name `should be equal to` entity.name - getAllChild().size `should be equal to` entity.childList.size - getAllChild() `should be equal to` entity.childList + getChildren().size `should be equal to` entity.childList.size + getChildren() `should be equal to` entity.childList .map { it.toDomainEntity() }.toMutableList() } } diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverterTest.kt index 041c71cd..7a5ed282 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverterTest.kt @@ -26,9 +26,9 @@ class APIPlaylistConverterTest { with(convertedEntity) { name `should be equal to` entity.name - getAllChild().size `should be equal to` entity.entriesList.size - getAllChild()[0] `should be equal to` entity.entriesList[0].toDomainEntity() - getAllChild()[1] `should be equal to` entity.entriesList[1].toDomainEntity() + getChildren().size `should be equal to` entity.entriesList.size + getChildren()[0] `should be equal to` entity.entriesList[0].toDomainEntity() + getChildren()[1] `should be equal to` entity.entriesList[1].toDomainEntity() } } From b33fe2d451adc96ea670c20c71bc5f48a87396da Mon Sep 17 00:00:00 2001 From: tzugen Date: Fri, 26 Nov 2021 19:01:14 +0100 Subject: [PATCH 15/33] Add nice looking empty list view Also fix shouldRetry() in the Downloader --- .../ultrasonic/fragment/ArtistListFragment.kt | 7 +- .../ultrasonic/fragment/DownloadsFragment.kt | 4 + .../ultrasonic/fragment/EntryListFragment.kt | 180 ++++++++++-------- .../ultrasonic/fragment/MultiListFragment.kt | 14 +- .../ultrasonic/fragment/SearchFragment.kt | 33 ++-- .../fragment/TrackCollectionFragment.kt | 7 +- .../moire/ultrasonic/model/AlbumListModel.kt | 4 +- .../moire/ultrasonic/model/ArtistListModel.kt | 4 +- .../moire/ultrasonic/model/SearchListModel.kt | 2 +- .../ultrasonic/model/TrackCollectionModel.kt | 3 + .../moire/ultrasonic/service/Downloader.kt | 9 +- ultrasonic/src/main/res/drawable/ic_empty.xml | 91 +++++++++ ultrasonic/src/main/res/layout/empty_view.xml | 38 ++++ .../src/main/res/layout/generic_list.xml | 1 + .../src/main/res/layout/recycler_view.xml | 7 - ultrasonic/src/main/res/layout/search.xml | 20 +- ultrasonic/src/main/res/layout/track_list.xml | 6 +- ultrasonic/src/main/res/values/strings.xml | 1 + 18 files changed, 281 insertions(+), 150 deletions(-) create mode 100644 ultrasonic/src/main/res/drawable/ic_empty.xml create mode 100644 ultrasonic/src/main/res/layout/empty_view.xml diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index d3169e1e..64732a45 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -59,15 +59,10 @@ class ArtistListFragment : EntryListFragment() { 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)) - bundle.putString( - Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, - Constants.ALPHABETICAL_BY_NAME - ) + bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST) bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) findNavController().navigate(itemClickTarget, bundle) } - - // Constants.ALPHABETICAL_BY_NAME } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index 70c70986..c3abfe03 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -11,6 +11,7 @@ import android.app.Application import android.os.Bundle import android.view.MenuItem import android.view.View +import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import org.koin.core.component.inject @@ -76,6 +77,9 @@ class DownloadsFragment : MultiListFragment() { val liveDataList = listModel.getList() + emptyTextView.setText(R.string.download_empty) + emptyView.isVisible = liveDataList.value?.isEmpty() ?: true + viewAdapter.submitList(liveDataList.value) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index af4f1947..6d26d137 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -7,8 +7,11 @@ import androidx.navigation.fragment.findNavController import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.GenericEntry +import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.service.RxBus +import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.util.Constants +import androidx.fragment.app.Fragment import org.moire.ultrasonic.util.Settings /** @@ -27,91 +30,11 @@ abstract class EntryListFragment : MultiListFragment() { !listModel.isOffline() && !Settings.shouldUseId3Tags } - @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 + return handleContextMenu(menuItem, item, isArtist, downloadHandler, this) } override fun onItemClick(item: T) { @@ -137,4 +60,97 @@ abstract class EntryListFragment : MultiListFragment() { listModel.refresh(refreshListView!!, arguments) } } + + companion object { + @Suppress("LongMethod") + internal fun handleContextMenu( + menuItem: MenuItem, + item: Identifiable, + isArtist: Boolean, + downloadHandler: DownloadHandler, + fragment: Fragment + ): Boolean { + when (menuItem.itemId) { + R.id.menu_play_now -> + downloadHandler.downloadRecursively( + fragment, + 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( + fragment, + 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( + fragment, + item.id, + save = false, + append = true, + autoPlay = false, + shuffle = false, + background = false, + playNext = false, + unpin = false, + isArtist = isArtist + ) + R.id.menu_pin -> + downloadHandler.downloadRecursively( + fragment, + item.id, + save = true, + append = true, + autoPlay = false, + shuffle = false, + background = false, + playNext = false, + unpin = false, + isArtist = isArtist + ) + R.id.menu_unpin -> + downloadHandler.downloadRecursively( + fragment, + item.id, + save = false, + append = false, + autoPlay = false, + shuffle = false, + background = false, + playNext = false, + unpin = true, + isArtist = isArtist + ) + R.id.menu_download -> + downloadHandler.downloadRecursively( + fragment, + item.id, + save = false, + append = false, + autoPlay = false, + shuffle = false, + background = true, + playNext = false, + unpin = false, + isArtist = isArtist + ) + } + return true + } + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index 635f7632..c932ecda 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -13,6 +13,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -46,6 +47,7 @@ abstract class MultiListFragment : Fragment() { protected var refreshListView: SwipeRefreshLayout? = null internal var listView: RecyclerView? = null internal lateinit var viewManager: LinearLayoutManager + internal lateinit var emptyView: ConstraintLayout internal lateinit var emptyTextView: TextView /** @@ -71,7 +73,7 @@ abstract class MultiListFragment : Fragment() { * The central function to pass a query to the model and return a LiveData object */ open fun getLiveData(args: Bundle? = null): LiveData> { - return MutableLiveData(listOf()) + return MutableLiveData() } /** @@ -90,7 +92,9 @@ abstract class MultiListFragment : Fragment() { */ open val refreshListId = R.id.swipe_refresh_view open val recyclerViewId = R.id.recycler_view - open val emptyTextViewId = R.id.empty_list_text + open val emptyViewId = R.id.empty_list_view + open val emptyTextId = R.id.empty_list_text + open fun setTitle(title: String?) { if (title == null) { @@ -121,14 +125,14 @@ abstract class MultiListFragment : Fragment() { liveDataItems = getLiveData(arguments) // Link view to display text if the list is empty - // FIXME: Hook this up globally. - emptyTextView = view.findViewById(emptyTextViewId) + emptyView = view.findViewById(emptyViewId) + emptyTextView = view.findViewById(emptyTextId) // Register an observer to update our UI when the data changes liveDataItems.observe( viewLifecycleOwner, { newItems -> - emptyTextView.isVisible = newItems.isEmpty() + emptyView.isVisible = newItems.isEmpty() viewAdapter.submitList(newItems) } ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 19553929..611f66af 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -28,6 +28,7 @@ import org.moire.ultrasonic.adapters.AlbumRowBinder import org.moire.ultrasonic.adapters.ArtistRowBinder import org.moire.ultrasonic.adapters.DividerBinder import org.moire.ultrasonic.adapters.TrackViewBinder +import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.SearchResult @@ -176,11 +177,11 @@ class SearchFragment : MultiListFragment(), KoinComponent { return search(query, autoPlay) } } - - // Fragment was started from the Menu, create empty list - // populateList(SearchResult()) } + /** + * This method create the search bar above the recycler view + */ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { val activity = activity ?: return val searchManager = activity.getSystemService(Context.SEARCH_SERVICE) as SearchManager @@ -191,8 +192,8 @@ class SearchFragment : MultiListFragment(), KoinComponent { searchView.setSearchableInfo(searchableInfo) val arguments = arguments - val autoPlay = - arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) + val autoPlay = arguments != null && + arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) val query = arguments?.getString(Constants.INTENT_EXTRA_NAME_QUERY) // If started with a query, enter it to the searchView @@ -211,13 +212,13 @@ class SearchFragment : MultiListFragment(), KoinComponent { val cursor = searchView.suggestionsAdapter.cursor cursor.moveToPosition(position) - // TODO: Try to do something with this magic const: - // 2 is the index of col containing suggestion name. + // 2 is the index of col containing suggestion name. val suggestion = cursor.getString(2) searchView.setQuery(suggestion, true) return true } }) + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { Timber.d("onQueryTextSubmit: %s", query) @@ -230,6 +231,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { return true } }) + searchView.setIconifiedByDefault(false) searchItem.expandActionView() } @@ -479,7 +481,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { } // Show/hide the empty text view - emptyTextView.isVisible = list.isEmpty() + emptyView.isVisible = list.isEmpty() viewAdapter.submitList(list) } @@ -557,20 +559,15 @@ class SearchFragment : MultiListFragment(), KoinComponent { var DEFAULT_SONGS = Settings.defaultSongs } - // FIXME!! - override fun getLiveData(args: Bundle?): LiveData> { - return MutableLiveData(listOf()) - } - // FIXME override val itemClickTarget: Int = 0 - // FIXME - override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean { - return true - } - // FIXME override fun onItemClick(item: Identifiable) { } + + override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean { + val isArtist = (item is Artist) + return EntryListFragment.handleContextMenu(menuItem, item, isArtist, downloadHandler, this) + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 6d15cc8d..5f1386f7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -57,7 +57,6 @@ import timber.log.Timber open class TrackCollectionFragment : MultiListFragment() { private var albumButtons: View? = null - private var emptyView: TextView? = null internal var selectButton: ImageView? = null internal var playNowButton: ImageView? = null internal var playNextButton: ImageView? = null @@ -107,8 +106,6 @@ open class TrackCollectionFragment : MultiListFragment() { setupButtons(view) - emptyView = view.findViewById(R.id.empty_list_text) - registerForContextMenu(listView!!) setHasOptionsMenu(true) @@ -579,7 +576,7 @@ open class TrackCollectionFragment : MultiListFragment() { } // Show a text if we have no entries - emptyView?.isVisible = entryList.isEmpty() + emptyView.isVisible = entryList.isEmpty() enableButtons() @@ -599,10 +596,8 @@ open class TrackCollectionFragment : MultiListFragment() { val albumHeader = AlbumHeader(it, name ?: intentAlbumName) val mixedList: MutableList = mutableListOf(albumHeader) mixedList.addAll(entryList) - Timber.e("SUBMITTING MIXED LIST") viewAdapter.submitList(mixedList) } else { - Timber.e("SUBMITTING ENTRY LIST") viewAdapter.submitList(entryList) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt index 24dfd80e..7ac20f1a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -13,7 +13,7 @@ import org.moire.ultrasonic.util.Settings class AlbumListModel(application: Application) : GenericListModel(application) { - val list: MutableLiveData> = MutableLiveData(listOf()) + val list: MutableLiveData> = MutableLiveData() var lastType: String? = null private var loadedUntil: Int = 0 @@ -26,7 +26,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) { // This way, we keep the scroll position val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!! - if (refresh || list.value!!.isEmpty() || albumListType != lastType) { + if (refresh || list.value?.isEmpty() != false || albumListType != lastType) { lastType = albumListType backgroundLoadFromServer(refresh, swipe, args) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt index ca2bed1f..8861a9ef 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt @@ -31,7 +31,7 @@ import org.moire.ultrasonic.service.MusicService * Provides ViewModel which contains the list of available Artists */ class ArtistListModel(application: Application) : GenericListModel(application) { - private val artists: MutableLiveData> = MutableLiveData(listOf()) + private val artists: MutableLiveData> = MutableLiveData() /** * Retrieves all available Artists in a LiveData @@ -39,7 +39,7 @@ class ArtistListModel(application: Application) : GenericListModel(application) fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> { // Don't reload the data if navigating back to the view that was active before. // This way, we keep the scroll position - if (artists.value!!.isEmpty() || refresh) { + if (artists.value?.isEmpty() != false || refresh) { backgroundLoadFromServer(refresh, swipe) } return artists diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt index 7bd1c3a4..a5907352 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt @@ -14,7 +14,7 @@ import org.moire.ultrasonic.util.Settings class SearchListModel(application: Application) : GenericListModel(application) { - var searchResult: MutableLiveData = MutableLiveData(null) + var searchResult: MutableLiveData = MutableLiveData() override fun load( isOffline: Boolean, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index ac9ef7c1..af8621d3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -19,6 +19,9 @@ import org.moire.ultrasonic.util.Util /* * Model for retrieving different collections of tracks from the API +* +* TODO: Remove double data keeping in currentList/currentDirectory and use the base model liveData +* For this refactor MusicService to replace MusicDirectories with List or List */ class TrackCollectionModel(application: Application) : GenericListModel(application) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 26e2e6c6..aa22751b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -157,7 +157,8 @@ class Downloader( // Add file to queue if not in one of the queues already. if (!download.isWorkDone && !activelyDownloading.contains(download) && - !downloadQueue.contains(download) + !downloadQueue.contains(download) && + download.shouldRetry() ) { listChanged = true downloadQueue.add(download) @@ -281,14 +282,18 @@ class Downloader( fun clearPlaylist() { playlist.clear() + val toRemove = mutableListOf() + // Cancel all active downloads with a high priority for (download in activelyDownloading) { if (download.priority < 100) { download.cancelDownload() - activelyDownloading.remove(download) + toRemove.add(download) } } + activelyDownloading.removeAll(toRemove) + playlistUpdateRevision++ updateLiveData() } diff --git a/ultrasonic/src/main/res/drawable/ic_empty.xml b/ultrasonic/src/main/res/drawable/ic_empty.xml new file mode 100644 index 00000000..74776517 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/ic_empty.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultrasonic/src/main/res/layout/empty_view.xml b/ultrasonic/src/main/res/layout/empty_view.xml new file mode 100644 index 00000000..b6e8bcce --- /dev/null +++ b/ultrasonic/src/main/res/layout/empty_view.xml @@ -0,0 +1,38 @@ + + + + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/generic_list.xml b/ultrasonic/src/main/res/layout/generic_list.xml index 45bce488..84164d10 100644 --- a/ultrasonic/src/main/res/layout/generic_list.xml +++ b/ultrasonic/src/main/res/layout/generic_list.xml @@ -4,6 +4,7 @@ a:layout_height="fill_parent" a:orientation="vertical"> + diff --git a/ultrasonic/src/main/res/layout/recycler_view.xml b/ultrasonic/src/main/res/layout/recycler_view.xml index 09ca5ccc..7808a71c 100644 --- a/ultrasonic/src/main/res/layout/recycler_view.xml +++ b/ultrasonic/src/main/res/layout/recycler_view.xml @@ -1,12 +1,5 @@ - - + diff --git a/ultrasonic/src/main/res/layout/track_list.xml b/ultrasonic/src/main/res/layout/track_list.xml index a53f6024..1190aefc 100644 --- a/ultrasonic/src/main/res/layout/track_list.xml +++ b/ultrasonic/src/main/res/layout/track_list.xml @@ -4,11 +4,7 @@ a:layout_height="fill_parent" a:orientation="vertical" > - - + diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index fe6010d8..9d217525 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -57,6 +57,7 @@ Do you want to delete %1$s Bookmark removed. Bookmark set at %s. + Nothing is downloading Playlist is empty Remote control is not allowed. Please enable jukebox mode in Users > Settings on your Subsonic server. Turned off remote control. Music is played on phone. From 82d90a6aee972d5d02b222354c20d66261498d7b Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 27 Nov 2021 00:51:41 +0100 Subject: [PATCH 16/33] Fix context menus. Also cleanup files, rename layouts --- .../moire/ultrasonic/domain/MusicDirectory.kt | 10 - ultrasonic/lint-baseline.xml | 573 ++++-------------- .../moire/ultrasonic/view/ArtistAdapter.java | 9 +- .../moire/ultrasonic/view/GenreAdapter.java | 4 +- .../ultrasonic/adapters/AlbumRowBinder.kt | 8 +- .../ultrasonic/adapters/ArtistRowBinder.kt | 11 +- .../moire/ultrasonic/adapters/BaseAdapter.kt | 2 +- .../ultrasonic/adapters/DividerBinder.kt | 2 +- .../adapters/FolderSelectorBinder.kt | 2 +- .../ultrasonic/adapters/HeaderViewBinder.kt | 2 +- .../org/moire/ultrasonic/adapters/Helper.kt | 22 - .../moire/ultrasonic/adapters/ImageHelper.kt | 45 -- .../ultrasonic/adapters/TrackViewBinder.kt | 40 +- .../ultrasonic/adapters/TrackViewHolder.kt | 27 +- .../org/moire/ultrasonic/adapters/Utils.kt | 72 +++ .../ultrasonic/fragment/AlbumListFragment.kt | 19 +- .../ultrasonic/fragment/ArtistListFragment.kt | 10 +- .../ultrasonic/fragment/BookmarksFragment.kt | 1 - .../ultrasonic/fragment/DownloadsFragment.kt | 34 +- .../ultrasonic/fragment/EntryListFragment.kt | 6 +- .../ultrasonic/fragment/MultiListFragment.kt | 9 +- .../ultrasonic/fragment/PlayerFragment.kt | 15 +- .../ultrasonic/fragment/SearchFragment.kt | 353 ++++------- .../fragment/TrackCollectionFragment.kt | 152 +++-- .../ultrasonic/model/GenericListModel.kt | 2 - .../ultrasonic/model/TrackCollectionModel.kt | 72 +-- .../service/AutoMediaBrowserService.kt | 6 +- .../ultrasonic/service/OfflineMusicService.kt | 6 +- .../ultrasonic/service/RESTMusicService.kt | 2 +- .../ultrasonic/subsonic/DownloadHandler.kt | 2 +- .../kotlin/org/moire/ultrasonic/util/Util.kt | 4 +- .../main/res/drawable/ic_baseline_info_24.xml | 10 - .../res/layout/album_list_item_legacy.xml | 51 -- ...album_header.xml => list_header_album.xml} | 0 ...lder_header.xml => list_header_folder.xml} | 0 ...lbum_list_item.xml => list_item_album.xml} | 11 +- ...ist_list_item.xml => list_item_artist.xml} | 19 +- ...{row_divider.xml => list_item_divider.xml} | 0 ...xt_list_item.xml => list_item_generic.xml} | 0 ...song_list_item.xml => list_item_track.xml} | 2 +- ...etails.xml => list_item_track_details.xml} | 0 ...neric_list.xml => list_layout_generic.xml} | 4 +- .../{track_list.xml => list_layout_track.xml} | 4 +- ...pty_view.xml => list_parts_empty_view.xml} | 0 ...ycler_view.xml => list_parts_recycler.xml} | 0 ultrasonic/src/main/res/layout/search.xml | 2 +- ...ntext_menu.xml => context_menu_artist.xml} | 0 .../src/main/res/menu/context_menu_track.xml | 26 + .../main/res/menu/generic_context_menu.xml | 26 - .../src/main/res/menu/select_song_context.xml | 26 - .../domain/APIAlbumConverterTest.kt | 4 +- .../domain/APIMusicDirectoryConverterTest.kt | 2 +- .../domain/APIPlaylistConverterTest.kt | 6 +- 53 files changed, 541 insertions(+), 1174 deletions(-) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Utils.kt delete mode 100644 ultrasonic/src/main/res/drawable/ic_baseline_info_24.xml delete mode 100644 ultrasonic/src/main/res/layout/album_list_item_legacy.xml rename ultrasonic/src/main/res/layout/{select_album_header.xml => list_header_album.xml} (100%) rename ultrasonic/src/main/res/layout/{select_folder_header.xml => list_header_folder.xml} (100%) rename ultrasonic/src/main/res/layout/{album_list_item.xml => list_item_album.xml} (90%) rename ultrasonic/src/main/res/layout/{artist_list_item.xml => list_item_artist.xml} (86%) rename ultrasonic/src/main/res/layout/{row_divider.xml => list_item_divider.xml} (100%) rename ultrasonic/src/main/res/layout/{generic_text_list_item.xml => list_item_generic.xml} (100%) rename ultrasonic/src/main/res/layout/{song_list_item.xml => list_item_track.xml} (97%) rename ultrasonic/src/main/res/layout/{song_details.xml => list_item_track_details.xml} (100%) rename ultrasonic/src/main/res/layout/{generic_list.xml => list_layout_generic.xml} (67%) rename ultrasonic/src/main/res/layout/{track_list.xml => list_layout_track.xml} (71%) rename ultrasonic/src/main/res/layout/{empty_view.xml => list_parts_empty_view.xml} (100%) rename ultrasonic/src/main/res/layout/{recycler_view.xml => list_parts_recycler.xml} (100%) rename ultrasonic/src/main/res/menu/{artist_context_menu.xml => context_menu_artist.xml} (100%) create mode 100644 ultrasonic/src/main/res/menu/context_menu_track.xml delete mode 100644 ultrasonic/src/main/res/menu/generic_context_menu.xml delete mode 100644 ultrasonic/src/main/res/menu/select_song_context.xml diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt index 49d02a4d..805088bd 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt @@ -8,16 +8,6 @@ import java.util.Date class MusicDirectory : ArrayList() { var name: String? = null - fun addFirst(child: Child) { - add(0, child) - } - - fun addChild(child: Child) { - add(child) - } - - fun findChild(id: String): GenericEntry? = lastOrNull { it.id == id } - @JvmOverloads fun getChildren( includeDirs: Boolean = true, diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index a3fa41b0..46313ca4 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -55,7 +55,7 @@ errorLine2=" ^"> @@ -66,29 +66,18 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + errorLine1=" <string name="select_album.n_selected">%d tracks selected</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - - @@ -136,17 +125,6 @@ column="10"/> - - - - + file="src/main/res/drawable/ic_baseline_info_24.xml" + line="1" + column="1"/> - + @@ -795,7 +773,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -806,7 +784,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -817,55 +795,55 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - + @@ -876,55 +854,55 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - + @@ -935,55 +913,55 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - + @@ -994,51 +972,51 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - + @@ -1064,47 +1042,47 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1194,17 +1172,6 @@ column="4"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1761,8 +1464,8 @@ errorLine1=" <ImageView" errorLine2=" ~~~~~~~~~"> @@ -1772,8 +1475,8 @@ errorLine1=" <ImageView" errorLine2=" ~~~~~~~~~"> @@ -1783,8 +1486,8 @@ errorLine1=" <ImageView" errorLine2=" ~~~~~~~~~"> @@ -1794,22 +1497,11 @@ errorLine1=" <ImageView" errorLine2=" ~~~~~~~~~"> - - - - - - - - - - - - - - - - diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ArtistAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ArtistAdapter.java index 04471395..b29de4e6 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ArtistAdapter.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ArtistAdapter.java @@ -19,8 +19,6 @@ package org.moire.ultrasonic.view; import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -28,6 +26,9 @@ import android.widget.ArrayAdapter; import android.widget.SectionIndexer; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.moire.ultrasonic.R; import org.moire.ultrasonic.domain.Artist; @@ -49,7 +50,7 @@ public class ArtistAdapter extends ArrayAdapter implements SectionIndexe public ArtistAdapter(Context context, List artists) { - super(context, R.layout.generic_text_list_item, artists); + super(context, R.layout.list_item_generic, artists); layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -81,7 +82,7 @@ public class ArtistAdapter extends ArrayAdapter implements SectionIndexe ) { View rowView = convertView; if (rowView == null) { - rowView = layoutInflater.inflate(R.layout.generic_text_list_item, parent, false); + rowView = layoutInflater.inflate(R.layout.list_item_generic, parent, false); } ((TextView) rowView).setText(getItem(position).getName()); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/GenreAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/GenreAdapter.java index 5b8f422e..475bb602 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/GenreAdapter.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/GenreAdapter.java @@ -48,7 +48,7 @@ public class GenreAdapter extends ArrayAdapter implements SectionIndexer public GenreAdapter(Context context, List genres) { - super(context, R.layout.generic_text_list_item, genres); + super(context, R.layout.list_item_generic, genres); layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -75,7 +75,7 @@ public class GenreAdapter extends ArrayAdapter implements SectionIndexer public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { View rowView = convertView; if (rowView == null) { - rowView = layoutInflater.inflate(R.layout.generic_text_list_item, parent, false); + rowView = layoutInflater.inflate(R.layout.list_item_generic, parent, false); } ((TextView) rowView).setText(getItem(position).getName()); diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt index c7ce06d0..2368f8fe 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt @@ -43,15 +43,15 @@ class AlbumRowBinder( Util.getDrawableFromAttribute(context, R.attr.star_hollow) // Set our layout files - val layout = R.layout.album_list_item - val contextMenuLayout = R.menu.artist_context_menu + val layout = R.layout.list_item_album + val contextMenuLayout = R.menu.context_menu_artist override fun onBindViewHolder(holder: ViewHolder, item: MusicDirectory.Album) { holder.album.text = item.title holder.artist.text = item.artist holder.details.setOnClickListener { onItemClick(item) } holder.details.setOnLongClickListener { - val popup = Helper.createPopupMenu(holder.itemView) + val popup = Utils.createPopupMenu(holder.itemView) popup.setOnMenuItemClickListener { menuItem -> onContextMenuClick(menuItem, item) @@ -78,7 +78,7 @@ class AlbumRowBinder( var album: TextView = view.findViewById(R.id.album_title) var artist: TextView = view.findViewById(R.id.album_artist) var details: LinearLayout = view.findViewById(R.id.row_album_details) - var coverArt: ImageView = view.findViewById(R.id.album_coverart) + var coverArt: ImageView = view.findViewById(R.id.coverart) var star: ImageView = view.findViewById(R.id.album_star) var coverArtId: String? = null } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt index 41a2e7a5..4a38f667 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt @@ -26,7 +26,6 @@ import org.moire.ultrasonic.util.Settings /** * Creates a Row in a RecyclerView which contains the details of an Artist - * FIXME: On click wrong display... */ class ArtistRowBinder( val onItemClick: (ArtistOrIndex) -> Unit, @@ -35,8 +34,8 @@ class ArtistRowBinder( private val enableSections: Boolean = true ) : ItemViewBinder(), KoinComponent { - val layout = R.layout.artist_list_item - val contextMenuLayout = R.menu.artist_context_menu + val layout = R.layout.list_item_artist + val contextMenuLayout = R.menu.context_menu_artist override fun onBindViewHolder(holder: ViewHolder, item: ArtistOrIndex) { holder.textView.text = item.name @@ -44,7 +43,7 @@ class ArtistRowBinder( holder.section.isVisible = enableSections holder.layout.setOnClickListener { onItemClick(item) } holder.layout.setOnLongClickListener { - val popup = Helper.createPopupMenu(holder.itemView) + val popup = Utils.createPopupMenu(holder.itemView, contextMenuLayout) popup.setOnMenuItemClickListener { menuItem -> onContextMenuClick(menuItem, item) @@ -106,8 +105,8 @@ class ArtistRowBinder( ) : RecyclerView.ViewHolder(itemView) { var section: TextView = itemView.findViewById(R.id.row_section) var textView: TextView = itemView.findViewById(R.id.row_artist_name) - var layout: RelativeLayout = itemView.findViewById(R.id.row_artist_layout) - var coverArt: ImageView = itemView.findViewById(R.id.artist_coverart) + var layout: RelativeLayout = itemView.findViewById(R.id.containing_layout) + var coverArt: ImageView = itemView.findViewById(R.id.coverart) var coverArtId: String? = null } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index 6e53062a..6230cf81 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -22,7 +22,7 @@ import org.moire.ultrasonic.util.BoundedTreeSet * The BaseAdapter which extends the MultiTypeAdapter from an external library. * It provides selection support as well as Diffing the submitted lists for performance. * - * It should be kept generic enought that it can be used a Base for all lists in the app. + * It should be kept generic enough that it can be used a Base for all lists in the app. */ class BaseAdapter : MultiTypeAdapter() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt index c8dcf395..679839a7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt @@ -15,7 +15,7 @@ import org.moire.ultrasonic.domain.Identifiable class DividerBinder : ItemViewBinder() { // Set our layout files - val layout = R.layout.row_divider + val layout = R.layout.list_item_divider override fun onBindViewHolder(holder: ViewHolder, item: Divider) { // Set text diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt index 83880ee8..b05bfd2c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt @@ -29,7 +29,7 @@ class FolderSelectorBinder(context: Context) : private val weakContext: WeakReference = WeakReference(context) // Set our layout files - val layout = R.layout.select_album_header + val layout = R.layout.list_header_folder override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder { return ViewHolder(inflater.inflate(layout, parent, false), weakContext) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt index 33826e48..d851c37e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt @@ -30,7 +30,7 @@ class HeaderViewBinder( private val imageLoaderProvider: ImageLoaderProvider by inject() // Set our layout files - val layout = R.layout.select_album_header + val layout = R.layout.list_header_album override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder { return ViewHolder(inflater.inflate(layout, parent, false)) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt deleted file mode 100644 index 4ed76b0a..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Helper.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.moire.ultrasonic.adapters - -import android.view.MenuInflater -import android.view.View -import android.widget.PopupMenu -import org.moire.ultrasonic.R -import org.moire.ultrasonic.data.ActiveServerProvider - -object Helper { - @JvmStatic - fun createPopupMenu(view: View, layout: Int = R.menu.artist_context_menu): PopupMenu { - val popup = PopupMenu(view.context, view) - val inflater: MenuInflater = popup.menuInflater - inflater.inflate(layout, popup.menu) - - val downloadMenuItem = popup.menu.findItem(R.id.menu_download) - downloadMenuItem?.isVisible = !ActiveServerProvider.isOffline() - - popup.show() - return popup - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt index 20cd02c5..c5aeb85f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt @@ -1,47 +1,2 @@ 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 errorImage: Drawable - 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) - errorImage = Util.getDrawableFromAttribute(context, R.attr.error) - downloadingImage = Util.getDrawableFromAttribute(context, R.attr.downloading) - playingImage = Util.getDrawableFromAttribute(context, R.attr.media_play_small) - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index 537847b5..e3f343ee 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -2,7 +2,7 @@ package org.moire.ultrasonic.adapters import android.content.Context import android.view.LayoutInflater -import android.view.View +import android.view.MenuItem import android.view.ViewGroup import androidx.lifecycle.LifecycleOwner import com.drakeet.multitype.ItemViewBinder @@ -15,24 +15,26 @@ import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.Downloader class TrackViewBinder( + val onItemClick: (DownloadFile) -> Unit, + val onContextMenuClick: ((MenuItem, DownloadFile) -> Boolean)? = null, val checkable: Boolean, val draggable: Boolean, context: Context, val lifecycleOwner: LifecycleOwner, - private val onClickCallback: ((View, DownloadFile?) -> Unit)? = null ) : ItemViewBinder(), KoinComponent { // Set our layout files - val layout = R.layout.song_list_item - val contextMenuLayout = R.menu.artist_context_menu + val layout = R.layout.list_item_track + val contextMenuLayout = R.menu.context_menu_track private val downloader: Downloader by inject() - private val imageHelper: ImageHelper = ImageHelper(context) + private val imageHelper: Utils.ImageHelper = Utils.ImageHelper(context) override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): TrackViewHolder { return TrackViewHolder(inflater.inflate(layout, parent, false)) } + @Suppress("LongMethod") override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) { val downloadFile: DownloadFile? val diffAdapter = adapter as BaseAdapter<*> @@ -58,6 +60,32 @@ class TrackViewBinder( diffAdapter.isSelected(item.longId) ) + holder.itemView.setOnLongClickListener { + if (onContextMenuClick != null) { + val popup = Utils.createPopupMenu(holder.itemView, contextMenuLayout) + + popup.setOnMenuItemClickListener { menuItem -> + onContextMenuClick?.invoke(menuItem, downloadFile) + } + } else { + // Minimize or maximize the Text view (if song title is very long) + if (!downloadFile.song.isDirectory) { + holder.maximizeOrMinimize() + } + } + + true + } + + holder.itemView.setOnClickListener { + if (!checkable) { + onItemClick(downloadFile) + } else { + val nowChecked = !holder.check.isChecked + holder.isChecked = nowChecked + } + } + // Notify the adapter of selection changes holder.observableChecked.observe( lifecycleOwner, @@ -95,7 +123,5 @@ class TrackViewBinder( holder.updateProgress(it) } ) - - holder.itemClickListener = onClickCallback } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 2732686f..9fc0a678 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -7,6 +7,7 @@ import android.widget.Checkable import android.widget.CheckedTextView import android.widget.ImageView import android.widget.LinearLayout +import android.widget.RelativeLayout import android.widget.TextView import androidx.core.view.isVisible import androidx.lifecycle.MutableLiveData @@ -29,6 +30,7 @@ import timber.log.Timber /** * Used to display songs and videos in a `ListView`. * FIXME: Add video List item + * FIXME: CHECKED bug */ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable, KoinComponent { @@ -47,8 +49,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable var duration: TextView = view.findViewById(R.id.song_duration) var progress: TextView = view.findViewById(R.id.song_status) - var itemClickListener: ((View, DownloadFile?) -> Unit)? = null - var entry: MusicDirectory.Entry? = null private set var downloadFile: DownloadFile? = null @@ -66,18 +66,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable features.isFeatureEnabled(Feature.FIVE_STAR_RATING) } - lateinit var imageHelper: ImageHelper - - init { - itemView.setOnClickListener { - if (itemClickListener != null) { - itemClickListener?.invoke(it, downloadFile) - } else { - val nowChecked = !check.isChecked - isChecked = nowChecked - } - } - } + lateinit var imageHelper: Utils.ImageHelper fun setSong( file: DownloadFile, @@ -85,7 +74,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable draggable: Boolean, isSelected: Boolean = false ) { - Timber.e("BINDING %s", isSelected) val song = file.song downloadFile = file entry = song @@ -125,15 +113,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable RxBus.playerStateObservable.subscribe { setPlayIcon(it.track == downloadFile) } - - // Minimize or maximize the Text view (if song title is very long) - itemView.setOnLongClickListener { - if (!song.isDirectory) { - maximizeOrMinimize() - true - } - false - } } private fun setPlayIcon(isPlaying: Boolean) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Utils.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Utils.kt new file mode 100644 index 00000000..e1ea9093 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Utils.kt @@ -0,0 +1,72 @@ +package org.moire.ultrasonic.adapters + +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.MenuInflater +import android.view.View +import android.widget.PopupMenu +import org.moire.ultrasonic.R +import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.util.Settings +import org.moire.ultrasonic.util.Util + +object Utils { + @JvmStatic + fun createPopupMenu(view: View, layout: Int = R.menu.context_menu_artist): PopupMenu { + val popup = PopupMenu(view.context, view) + val inflater: MenuInflater = popup.menuInflater + inflater.inflate(layout, popup.menu) + + val downloadMenuItem = popup.menu.findItem(R.id.menu_download) + downloadMenuItem?.isVisible = !ActiveServerProvider.isOffline() + + var shareButton = popup.menu.findItem(R.id.menu_item_share) + shareButton?.isVisible = !ActiveServerProvider.isOffline() + + shareButton = popup.menu.findItem(R.id.song_menu_share) + shareButton?.isVisible = !ActiveServerProvider.isOffline() + + popup.show() + return popup + } + + /** + * Provides cached drawables for the UI + */ + class ImageHelper(context: Context) { + + lateinit var errorImage: Drawable + 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) + errorImage = Util.getDrawableFromAttribute(context, R.attr.error) + downloadingImage = Util.getDrawableFromAttribute(context, R.attr.downloading) + playingImage = Util.getDrawableFromAttribute(context, R.attr.media_play_small) + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 49df1d34..94bf760e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -1,3 +1,10 @@ +/* + * AlbumListFragment.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.fragment import android.os.Bundle @@ -26,13 +33,7 @@ class AlbumListFragment : EntryListFragment() { /** * The id of the main layout */ - override val mainLayout: Int = R.layout.generic_list - - /** - * The id of the target in the navigation graph where we should go, - * after the user has clicked on an item - */ - override val itemClickTarget: Int = R.id.trackCollectionFragment + override val mainLayout: Int = R.layout.list_layout_generic /** * The central function to pass a query to the model and return a LiveData object @@ -71,6 +72,8 @@ class AlbumListFragment : EntryListFragment() { context = requireContext() ) ) + + emptyTextView.setText(R.string.select_album_empty) } override fun onItemClick(item: MusicDirectory.Album) { @@ -79,6 +82,6 @@ class AlbumListFragment : EntryListFragment() { bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, item.isDirectory) bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.title) bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent) - findNavController().navigate(itemClickTarget, bundle) + findNavController().navigate(R.id.trackCollectionFragment, bundle) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 64732a45..77e3d3f6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -25,13 +25,7 @@ class ArtistListFragment : EntryListFragment() { /** * The id of the main layout */ - override val mainLayout = R.layout.generic_list - - /** - * The id of the target in the navigation graph where we should go, - * after the user has clicked on an item - */ - override val itemClickTarget = R.id.selectArtistToSelectAlbum + override val mainLayout = R.layout.list_layout_generic /** * The central function to pass a query to the model and return a LiveData object @@ -63,6 +57,6 @@ class ArtistListFragment : EntryListFragment() { bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) - findNavController().navigate(itemClickTarget, bundle) + findNavController().navigate(R.id.selectArtistToSelectAlbum, bundle) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt index 54dfcccf..9fdcfa5c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -25,7 +25,6 @@ import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle * * Therefore this fragment allows only for singular selection and playback. * - * FIXME: use restore for playback */ class BookmarksFragment : TrackCollectionFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index c3abfe03..4d9250db 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -17,6 +17,7 @@ import androidx.lifecycle.LiveData import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.TrackViewBinder +import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.model.GenericListModel import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.Downloader @@ -26,8 +27,10 @@ import org.moire.ultrasonic.util.Util * Displays currently running downloads. * For now its a read-only view, there are no manipulations of the download list possible. * - * A consideration would be to base this class on TrackCollectionFragment and thereby inheriting the - * buttons useful to manipulate the list. + * TODO: A consideration would be to base this class on TrackCollectionFragment and thereby inheriting the + * buttons useful to manipulate the list. + * + * TODO: Add code to enable manipulation of the download list */ class DownloadsFragment : MultiListFragment() { @@ -36,13 +39,6 @@ class DownloadsFragment : MultiListFragment() { */ override val listModel: DownloadListModel by viewModels() - /** - * 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 - /** * The central function to pass a query to the model and return a LiveData object */ @@ -50,15 +46,6 @@ class DownloadsFragment : MultiListFragment() { return listModel.getList() } - override fun onContextMenuItemSelected(menuItem: MenuItem, item: DownloadFile): Boolean { - // TODO: Add code to enable manipulation of the download list - return true - } - - override fun onItemClick(item: DownloadFile) { - // TODO: Add code to enable manipulation of the download list - } - override fun setTitle(title: String?) { FragmentTitle.setTitle(this, Util.appContext().getString(R.string.menu_downloads)) } @@ -68,6 +55,8 @@ class DownloadsFragment : MultiListFragment() { viewAdapter.register( TrackViewBinder( + { }, + { _,_ -> true }, checkable = false, draggable = false, context = requireContext(), @@ -82,6 +71,15 @@ class DownloadsFragment : MultiListFragment() { viewAdapter.submitList(liveDataList.value) } + + override fun onContextMenuItemSelected(menuItem: MenuItem, item: DownloadFile): Boolean { + // TODO: Add code to enable manipulation of the download list + return true + } + + override fun onItemClick(item: DownloadFile) { + // TODO: Add code to enable manipulation of the download list + } } class DownloadListModel(application: Application) : GenericListModel(application) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 6d26d137..3961c4b2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -3,6 +3,7 @@ package org.moire.ultrasonic.fragment import android.os.Bundle import android.view.MenuItem import android.view.View +import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.Artist @@ -11,7 +12,6 @@ import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.util.Constants -import androidx.fragment.app.Fragment import org.moire.ultrasonic.util.Settings /** @@ -30,7 +30,6 @@ abstract class EntryListFragment : MultiListFragment() { !listModel.isOffline() && !Settings.shouldUseId3Tags } - override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean { val isArtist = (item is Artist) @@ -43,7 +42,7 @@ abstract class EntryListFragment : MultiListFragment() { 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) + findNavController().navigate(R.id.trackCollectionFragment, bundle) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -149,6 +148,7 @@ abstract class EntryListFragment : MultiListFragment() { unpin = false, isArtist = isArtist ) + else -> return false } return true } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index c932ecda..63b3484a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -76,16 +76,10 @@ abstract class MultiListFragment : Fragment() { return MutableLiveData() } - /** - * 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 main layout */ - open val mainLayout: Int = R.layout.generic_list + open val mainLayout: Int = R.layout.list_layout_generic /** * The ids of the swipe refresh view, the recycler view and the empty text view @@ -95,7 +89,6 @@ abstract class MultiListFragment : Fragment() { open val emptyViewId = R.id.empty_list_view open val emptyTextId = R.id.empty_list_text - open fun setTitle(title: String?) { if (title == null) { FragmentTitle.setTitle( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 3ca32373..57b7cec9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -229,7 +229,7 @@ class PlayerFragment : val ratingLinearLayout = view.findViewById(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(view.context, R.attr.star_full) fiveStar1ImageView.setOnClickListener { setSongRating(1) } fiveStar2ImageView.setOnClickListener { setSongRating(2) } @@ -561,14 +561,6 @@ class PlayerFragment : } } - override fun onContextItemSelected(menuItem: MenuItem): Boolean { - val info = menuItem.menuInfo as AdapterContextMenuInfo - val downloadFile = viewAdapter.getCurrentList()[info.position] as DownloadFile - return menuItemSelected(menuItem.itemId, downloadFile) || super.onContextItemSelected( - menuItem - ) - } - override fun onOptionsItemSelected(item: MenuItem): Boolean { return menuItemSelected(item.itemId, null) || super.onOptionsItemSelected(item) } @@ -856,7 +848,7 @@ class PlayerFragment : } // Create listener - val listener: ((View, DownloadFile?) -> Unit) = { _, file -> + val listener: ((DownloadFile) -> Unit) = { file -> val list = mediaPlayerController.playList val index = list.indexOf(file) mediaPlayerController.play(index) @@ -866,11 +858,12 @@ class PlayerFragment : viewAdapter.register( TrackViewBinder( + onItemClick = listener, + onContextMenuClick = {_,_ -> true}, checkable = false, draggable = true, context = requireContext(), lifecycleOwner = viewLifecycleOwner, - listener ) ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 611f66af..2050597e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -3,20 +3,15 @@ package org.moire.ultrasonic.fragment import android.app.SearchManager import android.content.Context import android.os.Bundle -import android.view.ContextMenu -import android.view.ContextMenu.ContextMenuInfo import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import android.widget.AdapterView.AdapterContextMenuInfo import android.widget.ListAdapter import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.fragment.app.viewModels -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import androidx.navigation.Navigation import androidx.swiperefreshlayout.widget.SwipeRefreshLayout @@ -34,6 +29,7 @@ import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.model.SearchListModel +import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.ShareHandler @@ -48,6 +44,8 @@ import timber.log.Timber /** * Initiates a search on the media library and displays the results + * + * FIXME: Handle context click on song */ class SearchFragment : MultiListFragment(), KoinComponent { private var moreArtistsButton: View? = null @@ -107,18 +105,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { // expandSongs() // } else { // val item = parent.getItemAtPosition(position) -// if (item is Artist) { -// onArtistSelected(item) -// } else if (item is MusicDirectory.Entry) { -// val entry = item -// if (entry.isDirectory) { -// onAlbumSelected(entry, false) -// } else if (entry.isVideo) { -// onVideoSelected(entry) -// } else { -// onSongSelected(entry, true) -// } -// } +// // } // }) @@ -129,13 +116,8 @@ class SearchFragment : MultiListFragment(), KoinComponent { // They need to be added in the order of most specific -> least specific. viewAdapter.register( ArtistRowBinder( - onItemClick = { entry -> onItemClick(entry) }, - onContextMenuClick = { menuItem, entry -> - onContextMenuItemSelected( - menuItem, - entry - ) - }, + onItemClick = ::onItemClick, + onContextMenuClick = ::onContextMenuItemSelected, imageLoader = imageLoaderProvider.getImageLoader(), enableSections = false ) @@ -143,13 +125,8 @@ class SearchFragment : MultiListFragment(), KoinComponent { viewAdapter.register( AlbumRowBinder( - onItemClick = { entry -> onItemClick(entry) }, - onContextMenuClick = { menuItem, entry -> - onContextMenuItemSelected( - menuItem, - entry - ) - }, + onItemClick = ::onItemClick, + onContextMenuClick = ::onContextMenuItemSelected, imageLoader = imageLoaderProvider.getImageLoader(), context = requireContext() ) @@ -157,6 +134,8 @@ class SearchFragment : MultiListFragment(), KoinComponent { viewAdapter.register( TrackViewBinder( + onItemClick = ::onItemClick, + onContextMenuClick = ::onContextMenuItemSelected, checkable = false, draggable = false, context = requireContext(), @@ -193,7 +172,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { val arguments = arguments val autoPlay = arguments != null && - arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) + arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) val query = arguments?.getString(Constants.INTENT_EXTRA_NAME_QUERY) // If started with a query, enter it to the searchView @@ -236,200 +215,11 @@ class SearchFragment : MultiListFragment(), KoinComponent { searchItem.expandActionView() } - // FIXME - override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) { - super.onCreateContextMenu(menu, view, menuInfo) - if (activity == null) return - val info = menuInfo as AdapterContextMenuInfo? -// val selectedItem = list!!.getItemAtPosition(info!!.position) -// val isArtist = selectedItem is Artist -// val isAlbum = selectedItem is MusicDirectory.Entry && selectedItem.isDirectory -// val inflater = requireActivity().menuInflater -// if (!isArtist && !isAlbum) { -// inflater.inflate(R.menu.select_song_context, menu) -// } else { -// inflater.inflate(R.menu.generic_context_menu, menu) -// } -// val shareButton = menu.findItem(R.id.menu_item_share) -// val downloadMenuItem = menu.findItem(R.id.menu_download) -// if (downloadMenuItem != null) { -// downloadMenuItem.isVisible = !isOffline() -// } -// if (isOffline() || isArtist) { -// if (shareButton != null) { -// shareButton.isVisible = false -// } -// } - } - - // FIXME - override fun onContextItemSelected(menuItem: MenuItem): Boolean { - val info = menuItem.menuInfo as AdapterContextMenuInfo -// val selectedItem = list!!.getItemAtPosition(info.position) -// val artist = if (selectedItem is Artist) selectedItem else null -// val entry = if (selectedItem is MusicDirectory.Entry) selectedItem else null -// var entryId: String? = null -// if (entry != null) { -// entryId = entry.id -// } -// val id = artist?.id ?: entryId ?: return true -// var songs: MutableList = ArrayList(1) -// val itemId = menuItem.itemId -// if (itemId == R.id.menu_play_now) { -// downloadHandler.downloadRecursively( -// this, -// id, -// false, -// false, -// true, -// false, -// false, -// false, -// false, -// false -// ) -// } else if (itemId == R.id.menu_play_next) { -// downloadHandler.downloadRecursively( -// this, -// id, -// false, -// true, -// false, -// true, -// false, -// true, -// false, -// false -// ) -// } else if (itemId == R.id.menu_play_last) { -// downloadHandler.downloadRecursively( -// this, -// id, -// false, -// true, -// false, -// false, -// false, -// false, -// false, -// false -// ) -// } else if (itemId == R.id.menu_pin) { -// downloadHandler.downloadRecursively( -// this, -// id, -// true, -// true, -// false, -// false, -// false, -// false, -// false, -// false -// ) -// } else if (itemId == R.id.menu_unpin) { -// downloadHandler.downloadRecursively( -// this, -// id, -// false, -// false, -// false, -// false, -// false, -// false, -// true, -// false -// ) -// } else if (itemId == R.id.menu_download) { -// downloadHandler.downloadRecursively( -// this, -// id, -// false, -// false, -// false, -// false, -// true, -// false, -// false, -// false -// ) -// } else if (itemId == R.id.song_menu_play_now) { -// if (entry != null) { -// songs = ArrayList(1) -// songs.add(entry) -// downloadHandler.download(this, false, false, true, false, false, songs) -// } -// } else if (itemId == R.id.song_menu_play_next) { -// if (entry != null) { -// songs = ArrayList(1) -// songs.add(entry) -// downloadHandler.download(this, true, false, false, true, false, songs) -// } -// } else if (itemId == R.id.song_menu_play_last) { -// if (entry != null) { -// songs = ArrayList(1) -// songs.add(entry) -// downloadHandler.download(this, true, false, false, false, false, songs) -// } -// } else if (itemId == R.id.song_menu_pin) { -// if (entry != null) { -// songs.add(entry) -// toast( -// context, -// resources.getQuantityString( -// R.plurals.select_album_n_songs_pinned, -// songs.size, -// songs.size -// ) -// ) -// downloadBackground(true, songs) -// } -// } else if (itemId == R.id.song_menu_download) { -// if (entry != null) { -// songs.add(entry) -// toast( -// context, -// resources.getQuantityString( -// R.plurals.select_album_n_songs_downloaded, -// songs.size, -// songs.size -// ) -// ) -// downloadBackground(false, songs) -// } -// } else if (itemId == R.id.song_menu_unpin) { -// if (entry != null) { -// songs.add(entry) -// toast( -// context, -// resources.getQuantityString( -// R.plurals.select_album_n_songs_unpinned, -// songs.size, -// songs.size -// ) -// ) -// mediaPlayerController.unpin(songs) -// } -// } else if (itemId == R.id.menu_item_share) { -// if (entry != null) { -// songs = ArrayList(1) -// songs.add(entry) -// shareHandler.createShare(this, songs, searchRefresh, cancellationToken!!) -// } -// return super.onContextItemSelected(menuItem) -// } else { -// return super.onContextItemSelected(menuItem) -// } - return true - } - - // OK! override fun onDestroyView() { cancellationToken?.cancel() super.onDestroyView() } - // OK! private fun downloadBackground(save: Boolean, songs: List) { val onValid = Runnable { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() @@ -515,13 +305,13 @@ class SearchFragment : MultiListFragment(), KoinComponent { // mergeAdapter!!.removeAdapter(moreSongsAdapter) // mergeAdapter!!.notifyDataSetChanged() // } -// -// private fun onArtistSelected(artist: Artist) { -// val bundle = Bundle() -// bundle.putString(Constants.INTENT_EXTRA_NAME_ID, artist.id) -// bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.id) -// Navigation.findNavController(requireView()).navigate(R.id.searchToSelectAlbum, bundle) -// } + + private fun onArtistSelected(artist: Artist) { + val bundle = Bundle() + bundle.putString(Constants.INTENT_EXTRA_NAME_ID, artist.id) + bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.id) + Navigation.findNavController(requireView()).navigate(R.id.searchToSelectAlbum, bundle) + } private fun onAlbumSelected(album: MusicDirectory.Album, autoplay: Boolean) { val bundle = Bundle() @@ -559,15 +349,112 @@ class SearchFragment : MultiListFragment(), KoinComponent { var DEFAULT_SONGS = Settings.defaultSongs } - // FIXME - override val itemClickTarget: Int = 0 - - // FIXME override fun onItemClick(item: Identifiable) { + when (item) { + is Artist -> { + onArtistSelected(item) + } + is MusicDirectory.Entry -> { + if (item.isVideo) { + onVideoSelected(item) + } else { + onSongSelected(item, true) + } + } + is MusicDirectory.Album -> { + onAlbumSelected(item, false) + } + } } + @Suppress("LongMethod") override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean { val isArtist = (item is Artist) - return EntryListFragment.handleContextMenu(menuItem, item, isArtist, downloadHandler, this) + val found = EntryListFragment.handleContextMenu(menuItem, item, isArtist, downloadHandler, this) + + if (found || item !is DownloadFile) return true + + val songs = mutableListOf() + + when (menuItem.itemId) { + R.id.song_menu_play_now -> { + songs.add(item.song) + downloadHandler.download( + fragment = this, + append = false, + save = false, + autoPlay = true, + playNext = false, + shuffle = false, + songs = songs + ) + } + R.id.song_menu_play_next -> { + songs.add(item.song) + downloadHandler.download( + fragment = this, + append = true, + save = false, + autoPlay = false, + playNext = true, + shuffle = false, + songs = songs + ) + } + R.id.song_menu_play_last -> { + songs.add(item.song) + downloadHandler.download( + fragment = this, + append = true, + save = false, + autoPlay = false, + playNext = false, + shuffle = false, + songs = songs + ) + } + R.id.song_menu_pin -> { + songs.add(item.song) + toast( + context, + resources.getQuantityString( + R.plurals.select_album_n_songs_pinned, + songs.size, + songs.size + ) + ) + downloadBackground(true, songs) + } + R.id.song_menu_download -> { + songs.add(item.song) + toast( + context, + resources.getQuantityString( + R.plurals.select_album_n_songs_downloaded, + songs.size, + songs.size + ) + ) + downloadBackground(false, songs) + } + R.id.song_menu_unpin -> { + songs.add(item.song) + toast( + context, + resources.getQuantityString( + R.plurals.select_album_n_songs_unpinned, + songs.size, + songs.size + ) + ) + mediaPlayerController.unpin(songs) + } + R.id.song_menu_share -> { + songs.add(item.song) + shareHandler.createShare(this, songs, searchRefresh, cancellationToken!!) + } + } + + return true } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 5f1386f7..89c34c7d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -14,9 +14,7 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import android.widget.AdapterView.AdapterContextMenuInfo import android.widget.ImageView -import android.widget.TextView import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData @@ -81,13 +79,7 @@ open class TrackCollectionFragment : MultiListFragment() { /** * The id of the main layout */ - override val mainLayout: Int = R.layout.track_list - - /** - * The id of the target in the navigation graph where we should go, - * after the user has clicked on an item - */ - override val itemClickTarget: Int = R.id.trackCollectionFragment + override val mainLayout: Int = R.layout.list_layout_track override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -127,6 +119,8 @@ open class TrackCollectionFragment : MultiListFragment() { viewAdapter.register( TrackViewBinder( + onItemClick = { onItemClick(it.song) }, + onContextMenuClick = { menu, id -> onContextMenuItemSelected(menu, id.song) }, checkable = true, draggable = false, context = requireContext(), @@ -209,78 +203,6 @@ open class TrackCollectionFragment : MultiListFragment() { getLiveData(args) } - override fun onContextItemSelected(menuItem: MenuItem): Boolean { - Timber.d("onContextItemSelected") - val info = menuItem.menuInfo as AdapterContextMenuInfo? ?: return true - - val entry = viewAdapter.getCurrentList()[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 = ArrayList(1) - entries.add(entry) - shareHandler.createShare( - this, entries, refreshListView, - cancellationToken!! - ) - return true - } - else -> { - return super.onContextItemSelected(menuItem) - } - } - return true - } - override fun onPrepareOptionsMenu(menu: Menu) { super.onPrepareOptionsMenu(menu) playAllButton = menu.findItem(R.id.select_album_play_all) @@ -503,7 +425,7 @@ open class TrackCollectionFragment : MultiListFragment() { private val songsForGenreObserver = Observer { musicDirectory -> // Hide more button when results are less than album list size - if (musicDirectory.getChildren().size < requireArguments().getInt( + if (musicDirectory.size < requireArguments().getInt( Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0 ) ) { @@ -700,12 +622,74 @@ open class TrackCollectionFragment : MultiListFragment() { return listModel.currentList } + @Suppress("LongMethod") override fun onContextMenuItemSelected( menuItem: MenuItem, item: MusicDirectory.Entry ): Boolean { - // TODO - return false + val entryId = item.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 = ArrayList(1) + entries.add(item) + shareHandler.createShare( + this, entries, refreshListView, + cancellationToken!! + ) + return true + } + else -> { + return super.onContextItemSelected(menuItem) + } + } + return true } override fun onItemClick(item: MusicDirectory.Entry) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt index 497b9e9f..5c3663ae 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt @@ -118,6 +118,4 @@ open class GenericListModel(application: Application) : internal fun hasOnlyFolders(musicDirectory: MusicDirectory) = musicDirectory.getChildren(includeDirs = true, includeFiles = false).size == musicDirectory.getChildren(includeDirs = true, includeFiles = true).size - - internal val allSongsId = "-1" } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index af8621d3..3fbbf45a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -9,7 +9,6 @@ package org.moire.ultrasonic.model import android.app.Application import androidx.lifecycle.MutableLiveData -import java.util.LinkedList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.moire.ultrasonic.domain.MusicDirectory @@ -38,33 +37,15 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() + val musicDirectory = service.getMusicDirectory(id, name, refresh) - var root = MusicDirectory() - - if (allSongsId == id && parentId != null) { - val musicDirectory = service.getMusicDirectory( - parentId, name, refresh - ) - - val songs: MutableList = LinkedList() - getSongsRecursively(musicDirectory, songs) - - for (song in songs) { - if (!song.isDirectory) { - root.addChild(song) - } - } - } else { - val musicDirectory = service.getMusicDirectory(id, name, refresh) - root = musicDirectory - } - - currentDirectory.postValue(root) - updateList(root) + currentDirectory.postValue(musicDirectory) + updateList(musicDirectory) } } // Given a Music directory "songs" it recursively adds all children to "songs" + @Suppress("unused") private fun getSongsRecursively( parent: MusicDirectory, songs: MutableList @@ -78,13 +59,8 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } for ((id1, _, _, title) in parent.getAlbums()) { - var root: MusicDirectory - - if (allSongsId != id1) { - root = service.getMusicDirectory(id1, title, false) - - getSongsRecursively(root, songs) - } + val root: MusicDirectory = service.getMusicDirectory(id1, title, false) + getSongsRecursively(root, songs) } } @@ -93,39 +69,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() - - val musicDirectory: MusicDirectory - - if (allSongsId == id && parentId != null) { - val root = MusicDirectory() - - val songs: MutableCollection = LinkedList() - val artist = service.getArtist(parentId, "", false) - - // FIXME is still working? - for ((id1) in artist) { - if (allSongsId != id1) { - val albumDirectory = service.getAlbum( - id1, "", false - ) - - for (song in albumDirectory.getTracks()) { - if (!song.isVideo) { - songs.add(song) - } - } - } - } - - for (song in songs) { - if (!song.isDirectory) { - root.addChild(song) - } - } - musicDirectory = root - } else { - musicDirectory = service.getAlbum(id, name, refresh) - } + val musicDirectory: MusicDirectory = service.getAlbum(id, name, refresh) currentDirectory.postValue(musicDirectory) updateList(musicDirectory) @@ -217,7 +161,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat for (share in shares) { if (share.id == shareId) { for (entry in share.getEntries()) { - musicDirectory.addChild(entry) + musicDirectory.add(entry) } break } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt index c44d202f..a8c8b91c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt @@ -584,7 +584,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { ) } - if (albums?.getChildren()?.count() ?: 0 >= DISPLAY_LIMIT) + if (albums?.size ?: 0 >= DISPLAY_LIMIT) mediaItems.add( R.string.search_more, listOf(MEDIA_ALBUM_PAGE_ID, type.typeName, (page ?: 0) + 1).joinToString("|"), @@ -626,7 +626,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val content = callWithErrorHandling { musicService.getPlaylist(id, name) } if (content != null) { - if (content.getChildren().count() > 1) + if (content.size > 1) mediaItems.addPlayAllItem( listOf(MEDIA_PLAYLIST_ITEM, id, name).joinToString("|") ) @@ -928,7 +928,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val songs = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) } if (songs != null) { - if (songs.getChildren().count() > 1) + if (songs.size > 1) mediaItems.addPlayAllItem(listOf(MEDIA_SONG_RANDOM_ID).joinToString("|")) // TODO: Paging is not implemented for songs, is it necessary at all? diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index d1b26e5c..5ad9d53f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -109,7 +109,7 @@ class OfflineMusicService : MusicService, KoinComponent { val filename = getName(file) if (filename != null && !seen.contains(filename)) { seen.add(filename) - result.addChild(createEntry(file, filename)) + result.add(createEntry(file, filename)) } } @@ -207,7 +207,7 @@ class OfflineMusicService : MusicService, KoinComponent { val entryFile = File(line) val entryName = getName(entryFile) if (entryFile.exists() && entryName != null) { - playlist.addChild(createEntry(entryFile, entryName)) + playlist.add(createEntry(entryFile, entryName)) } } playlist @@ -260,7 +260,7 @@ class OfflineMusicService : MusicService, KoinComponent { val finalSize: Int = children.size.coerceAtMost(size) for (i in 0 until finalSize) { val file = children[i % children.size] - result.addChild(createEntry(file, getName(file))) + result.add(createEntry(file, getName(file))) } return result } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 9ce8f92b..3f89c136 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -319,7 +319,7 @@ open class RESTMusicService( ) { val entry = podcastEntry.toDomainEntity() entry.track = null - musicDirectory.addChild(entry) + musicDirectory.add(entry) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index 7ae5ba08..9bb0a61f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -219,7 +219,7 @@ class DownloadHandler( for (share in shares) { if (share.id == id) { for (entry in share.getEntries()) { - root.addChild(entry) + root.add(entry) } break } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index a8d798bf..6168242f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -519,7 +519,7 @@ object Util { fun getSongsFromSearchResult(searchResult: SearchResult): MusicDirectory { val musicDirectory = MusicDirectory() for (entry in searchResult.songs) { - musicDirectory.addChild(entry) + musicDirectory.add(entry) } return musicDirectory } @@ -531,7 +531,7 @@ object Util { for (bookmark in bookmarks) { song = bookmark.entry song.bookmarkPosition = bookmark.position - musicDirectory.addChild(song) + musicDirectory.add(song) } return musicDirectory } diff --git a/ultrasonic/src/main/res/drawable/ic_baseline_info_24.xml b/ultrasonic/src/main/res/drawable/ic_baseline_info_24.xml deleted file mode 100644 index 17255b7a..00000000 --- a/ultrasonic/src/main/res/drawable/ic_baseline_info_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/layout/album_list_item_legacy.xml b/ultrasonic/src/main/res/layout/album_list_item_legacy.xml deleted file mode 100644 index f8e244a2..00000000 --- a/ultrasonic/src/main/res/layout/album_list_item_legacy.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/ultrasonic/src/main/res/layout/select_album_header.xml b/ultrasonic/src/main/res/layout/list_header_album.xml similarity index 100% rename from ultrasonic/src/main/res/layout/select_album_header.xml rename to ultrasonic/src/main/res/layout/list_header_album.xml diff --git a/ultrasonic/src/main/res/layout/select_folder_header.xml b/ultrasonic/src/main/res/layout/list_header_folder.xml similarity index 100% rename from ultrasonic/src/main/res/layout/select_folder_header.xml rename to ultrasonic/src/main/res/layout/list_header_folder.xml diff --git a/ultrasonic/src/main/res/layout/album_list_item.xml b/ultrasonic/src/main/res/layout/list_item_album.xml similarity index 90% rename from ultrasonic/src/main/res/layout/album_list_item.xml rename to ultrasonic/src/main/res/layout/list_item_album.xml index e3e971d1..d3bee2ba 100644 --- a/ultrasonic/src/main/res/layout/album_list_item.xml +++ b/ultrasonic/src/main/res/layout/list_item_album.xml @@ -2,7 +2,7 @@ + tools:src="@drawable/ic_star_hollow_dark" + a:contentDescription="@string/download.menu_star" /> diff --git a/ultrasonic/src/main/res/layout/artist_list_item.xml b/ultrasonic/src/main/res/layout/list_item_artist.xml similarity index 86% rename from ultrasonic/src/main/res/layout/artist_list_item.xml rename to ultrasonic/src/main/res/layout/list_item_artist.xml index 527523c8..84782315 100644 --- a/ultrasonic/src/main/res/layout/artist_list_item.xml +++ b/ultrasonic/src/main/res/layout/list_item_artist.xml @@ -1,9 +1,10 @@ @@ -17,17 +18,17 @@ a:minHeight="56dip" a:paddingStart="8dip" a:paddingEnd="8dip" - a:text="A" a:textAppearance="?android:attr/textAppearanceLarge" - a:textColor="@color/cyan" /> + a:textColor="@color/cyan" + tools:text="A" /> + \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/row_divider.xml b/ultrasonic/src/main/res/layout/list_item_divider.xml similarity index 100% rename from ultrasonic/src/main/res/layout/row_divider.xml rename to ultrasonic/src/main/res/layout/list_item_divider.xml diff --git a/ultrasonic/src/main/res/layout/generic_text_list_item.xml b/ultrasonic/src/main/res/layout/list_item_generic.xml similarity index 100% rename from ultrasonic/src/main/res/layout/generic_text_list_item.xml rename to ultrasonic/src/main/res/layout/list_item_generic.xml diff --git a/ultrasonic/src/main/res/layout/song_list_item.xml b/ultrasonic/src/main/res/layout/list_item_track.xml similarity index 97% rename from ultrasonic/src/main/res/layout/song_list_item.xml rename to ultrasonic/src/main/res/layout/list_item_track.xml index 74f7aa7c..c2c140af 100644 --- a/ultrasonic/src/main/res/layout/song_list_item.xml +++ b/ultrasonic/src/main/res/layout/list_item_track.xml @@ -26,7 +26,7 @@ a:gravity="center_vertical" a:paddingEnd="4dip"/> - + - - + + diff --git a/ultrasonic/src/main/res/layout/track_list.xml b/ultrasonic/src/main/res/layout/list_layout_track.xml similarity index 71% rename from ultrasonic/src/main/res/layout/track_list.xml rename to ultrasonic/src/main/res/layout/list_layout_track.xml index 1190aefc..117592a2 100644 --- a/ultrasonic/src/main/res/layout/track_list.xml +++ b/ultrasonic/src/main/res/layout/list_layout_track.xml @@ -4,8 +4,8 @@ a:layout_height="fill_parent" a:orientation="vertical" > - - + + diff --git a/ultrasonic/src/main/res/layout/empty_view.xml b/ultrasonic/src/main/res/layout/list_parts_empty_view.xml similarity index 100% rename from ultrasonic/src/main/res/layout/empty_view.xml rename to ultrasonic/src/main/res/layout/list_parts_empty_view.xml diff --git a/ultrasonic/src/main/res/layout/recycler_view.xml b/ultrasonic/src/main/res/layout/list_parts_recycler.xml similarity index 100% rename from ultrasonic/src/main/res/layout/recycler_view.xml rename to ultrasonic/src/main/res/layout/list_parts_recycler.xml diff --git a/ultrasonic/src/main/res/layout/search.xml b/ultrasonic/src/main/res/layout/search.xml index 1fab33c0..513054b2 100644 --- a/ultrasonic/src/main/res/layout/search.xml +++ b/ultrasonic/src/main/res/layout/search.xml @@ -5,7 +5,7 @@ a:layout_height="match_parent" a:orientation="vertical"> - + + + + + + + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/menu/generic_context_menu.xml b/ultrasonic/src/main/res/menu/generic_context_menu.xml deleted file mode 100644 index 553bf63e..00000000 --- a/ultrasonic/src/main/res/menu/generic_context_menu.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/ultrasonic/src/main/res/menu/select_song_context.xml b/ultrasonic/src/main/res/menu/select_song_context.xml deleted file mode 100644 index 3a534b25..00000000 --- a/ultrasonic/src/main/res/menu/select_song_context.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIAlbumConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIAlbumConverterTest.kt index c5f0d917..4d8eab69 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIAlbumConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIAlbumConverterTest.kt @@ -50,8 +50,8 @@ class APIAlbumConverterTest { with(convertedEntity) { name `should be equal to` null - getChildren().size `should be equal to` entity.songList.size - getChildren()[0] `should be equal to` entity.songList[0].toDomainEntity() + size `should be equal to` entity.songList.size + this[0] `should be equal to` entity.songList[0].toDomainEntity() } } diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverterTest.kt index 938d1807..e1b626cb 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverterTest.kt @@ -24,7 +24,7 @@ class APIMusicDirectoryConverterTest { with(convertedEntity) { name `should be equal to` entity.name - getChildren().size `should be equal to` entity.childList.size + size `should be equal to` entity.childList.size getChildren() `should be equal to` entity.childList .map { it.toDomainEntity() }.toMutableList() } diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverterTest.kt index 7a5ed282..7c06d540 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverterTest.kt @@ -26,9 +26,9 @@ class APIPlaylistConverterTest { with(convertedEntity) { name `should be equal to` entity.name - getChildren().size `should be equal to` entity.entriesList.size - getChildren()[0] `should be equal to` entity.entriesList[0].toDomainEntity() - getChildren()[1] `should be equal to` entity.entriesList[1].toDomainEntity() + size `should be equal to` entity.entriesList.size + this[0] `should be equal to` entity.entriesList[0].toDomainEntity() + this[1] `should be equal to` entity.entriesList[1].toDomainEntity() } } From 2f0ff384d00b6f303ce2dea10106db470c515616 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 28 Nov 2021 18:26:44 +0100 Subject: [PATCH 17/33] Various fixes * Work on folder selector, * Make current play queue drag&droppable * Fix album view in offline mode --- .../moire/ultrasonic/adapters/BaseAdapter.kt | 14 +------ .../adapters/FolderSelectorBinder.kt | 27 ++++++++----- .../moire/ultrasonic/adapters/ImageHelper.kt | 1 - .../ultrasonic/adapters/TrackViewBinder.kt | 2 +- .../ultrasonic/adapters/TrackViewHolder.kt | 1 - .../ultrasonic/fragment/AlbumListFragment.kt | 38 +++++++++++++++++++ .../ultrasonic/fragment/ArtistListFragment.kt | 24 +++++++++--- .../ultrasonic/fragment/DownloadsFragment.kt | 5 +-- .../ultrasonic/fragment/EntryListFragment.kt | 7 +++- .../ultrasonic/fragment/MultiListFragment.kt | 19 +++++----- .../ultrasonic/fragment/PlayerFragment.kt | 9 ++--- .../ultrasonic/fragment/SearchFragment.kt | 9 ++++- .../fragment/TrackCollectionFragment.kt | 14 +++---- .../ultrasonic/model/GenericListModel.kt | 13 +++++-- .../moire/ultrasonic/service/Downloader.kt | 14 +++++++ .../service/MediaPlayerController.kt | 5 +++ .../moire/ultrasonic/util/DragSortCallback.kt | 28 -------------- .../main/res/navigation/navigation_graph.xml | 10 ++++- 18 files changed, 150 insertions(+), 90 deletions(-) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index 6230cf81..f2a64633 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -24,6 +24,7 @@ import org.moire.ultrasonic.util.BoundedTreeSet * * It should be kept generic enough that it can be used a Base for all lists in the app. */ +@Suppress("unused", "UNUSED_PARAMETER") class BaseAdapter : MultiTypeAdapter() { // Update the BoundedTreeSet if selection type is changed @@ -195,19 +196,6 @@ class BaseAdapter : MultiTypeAdapter() { return selectedSet.contains(longId) } - fun moveItem(from: Int, to: Int): List { - val list = getCurrentList().toMutableList() - val fromLocation = list[from] - list.removeAt(from) - if (to < from) { - list.add(to + 1, fromLocation) - } else { - list.add(to - 1, fromLocation) - } - submitList(list) - return list - } - fun hasSingleSelection(): Boolean { return selectionType == SelectionType.SINGLE } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt index b05bfd2c..296e55e4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt @@ -36,15 +36,22 @@ class FolderSelectorBinder(context: Context) : } override fun onBindViewHolder(holder: ViewHolder, item: FolderHeader) { - holder.setData(item.selected, item.folders) + holder.setData(item) } class ViewHolder( view: View, private val weakContext: WeakReference ) : RecyclerView.ViewHolder(view) { - private var musicFolders: List = mutableListOf() - private var selectedFolderId: String? = null + + private var data: FolderHeader? = null + + private val selectedFolderId: String? + get() = data?.selected + + private val musicFolders: List + get() = data?.folders ?: mutableListOf() + private val folderName: TextView = itemView.findViewById(R.id.select_folder_name) private val layout: LinearLayout = itemView.findViewById(R.id.select_folder_header) @@ -53,9 +60,8 @@ class FolderSelectorBinder(context: Context) : layout.setOnClickListener { onFolderClick() } } - fun setData(selectedId: String?, folders: List) { - selectedFolderId = selectedId - musicFolders = folders + fun setData(item: FolderHeader) { + data = item if (selectedFolderId != null) { for ((id, name) in musicFolders) { if (id == selectedFolderId) { @@ -74,9 +80,11 @@ class FolderSelectorBinder(context: Context) : var menuItem = popup.menu.add( MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders ) + if (selectedFolderId == null || selectedFolderId!!.isEmpty()) { menuItem.isChecked = true } + musicFolders.forEachIndexed { i, musicFolder -> val (id, name) = musicFolder menuItem = popup.menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, name) @@ -95,7 +103,8 @@ class FolderSelectorBinder(context: Context) : val selectedFolder = if (menuItem.itemId == -1) null else musicFolders[menuItem.itemId] val musicFolderName = selectedFolder?.name ?: weakContext.get()!!.getString(R.string.select_artist_all_folders) - selectedFolderId = selectedFolder?.id + + data?.selected = selectedFolder?.id menuItem.isChecked = true folderName.text = musicFolderName @@ -111,8 +120,8 @@ class FolderSelectorBinder(context: Context) : } data class FolderHeader( - val folders: List, - val selected: String? + var folders: List, + var selected: String? ) : Identifiable { override val id: String get() = "FOLDERSELECTOR" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt index c5aeb85f..5a3abd0b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt @@ -1,2 +1 @@ package org.moire.ultrasonic.adapters - diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index e3f343ee..00d53800 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -65,7 +65,7 @@ class TrackViewBinder( val popup = Utils.createPopupMenu(holder.itemView, contextMenuLayout) popup.setOnMenuItemClickListener { menuItem -> - onContextMenuClick?.invoke(menuItem, downloadFile) + onContextMenuClick?.invoke(menuItem, downloadFile) } } else { // Minimize or maximize the Text view (if song title is very long) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 9fc0a678..a0e98132 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -7,7 +7,6 @@ import android.widget.Checkable import android.widget.CheckedTextView import android.widget.ImageView import android.widget.LinearLayout -import android.widget.RelativeLayout import android.widget.TextView import androidx.core.view.isVisible import androidx.lifecycle.MutableLiveData diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 94bf760e..30a0a601 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -9,12 +9,15 @@ package org.moire.ultrasonic.fragment import android.os.Bundle import android.view.View +import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.RecyclerView import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.AlbumRowBinder +import org.moire.ultrasonic.adapters.FolderSelectorBinder +import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.model.AlbumListModel import org.moire.ultrasonic.util.Constants @@ -84,4 +87,39 @@ class AlbumListFragment : EntryListFragment() { bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent) findNavController().navigate(R.id.trackCollectionFragment, bundle) } + + /** + * What to do when the list has changed + */ + override val defaultObserver: (List) -> Unit = { + emptyView.isVisible = it.isEmpty() + + if (showFolderHeader()) { + @Suppress("UNCHECKED_CAST") + val list = it as MutableList + list.add(0, folderHeader) + } else { + viewAdapter.submitList(it) + } + } + + /** + * Get a folder header and update it on changes + */ + private val folderHeader: FolderSelectorBinder.FolderHeader by lazy { + val header = FolderSelectorBinder.FolderHeader( + listModel.musicFolders.value!!, + listModel.activeServer.musicFolderId + ) + + listModel.musicFolders.observe( + viewLifecycleOwner, + { + header.folders = it + viewAdapter.notifyItemChanged(0) + } + ) + + header + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 77e3d3f6..1ec67ac3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -9,6 +9,7 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.ArtistRowBinder import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.ArtistOrIndex +import org.moire.ultrasonic.domain.Index import org.moire.ultrasonic.model.ArtistListModel import org.moire.ultrasonic.util.Constants @@ -47,16 +48,29 @@ class ArtistListFragment : EntryListFragment() { ) } + /** + * There are different targets depending on what list we show. + * If we are showing indexes, we need to go to TrackCollection + * If we are showing artists, we need to go to AlbumList + */ override fun onItemClick(item: ArtistOrIndex) { val bundle = Bundle() + + // Common arguments 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)) - bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST) - bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) - findNavController().navigate(R.id.selectArtistToSelectAlbum, bundle) + + // Check type + if (item is Index) { + findNavController().navigate(R.id.artistsListToTrackCollection, bundle) + } else { + bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST) + bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) + bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) + bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) + findNavController().navigate(R.id.artistsListToAlbumsList, bundle) + } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index 4d9250db..1ad3c658 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -17,7 +17,6 @@ import androidx.lifecycle.LiveData import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.TrackViewBinder -import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.model.GenericListModel import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.Downloader @@ -56,7 +55,7 @@ class DownloadsFragment : MultiListFragment() { viewAdapter.register( TrackViewBinder( { }, - { _,_ -> true }, + { _, _ -> true }, checkable = false, draggable = false, context = requireContext(), @@ -78,7 +77,7 @@ class DownloadsFragment : MultiListFragment() { } override fun onItemClick(item: DownloadFile) { - // TODO: Add code to enable manipulation of the download list + // TODO: Add code to enable manipulation of the download list } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 3961c4b2..44dc1aef 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -6,6 +6,7 @@ import android.view.View import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import org.moire.ultrasonic.R +import org.moire.ultrasonic.adapters.FolderSelectorBinder import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.GenericEntry import org.moire.ultrasonic.domain.Identifiable @@ -24,7 +25,6 @@ abstract class EntryListFragment : MultiListFragment() { /** * Whether to show the folder selector */ - // FIXME fun showFolderHeader(): Boolean { return listModel.showSelectFolderHeader(arguments) && !listModel.isOffline() && !Settings.shouldUseId3Tags @@ -55,9 +55,14 @@ abstract class EntryListFragment : MultiListFragment() { currentSetting.musicFolderId = it serverSettingsModel.updateItem(currentSetting) } + // FIXME: Needed? viewAdapter.notifyDataSetChanged() listModel.refresh(refreshListView!!, arguments) } + + viewAdapter.register( + FolderSelectorBinder(view.context) + ) } companion object { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index 63b3484a..5588cebb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -102,6 +102,14 @@ abstract class MultiListFragment : Fragment() { } } + /** + * What to do when the list has changed + */ + internal open val defaultObserver: ((List) -> Unit) = { + emptyView.isVisible = it.isEmpty() + viewAdapter.submitList(it) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -122,13 +130,7 @@ abstract class MultiListFragment : Fragment() { emptyTextView = view.findViewById(emptyTextId) // Register an observer to update our UI when the data changes - liveDataItems.observe( - viewLifecycleOwner, - { newItems -> - emptyView.isVisible = newItems.isEmpty() - viewAdapter.submitList(newItems) - } - ) + liveDataItems.observe(viewLifecycleOwner, defaultObserver) // Create a View Manager viewManager = LinearLayoutManager(this.context) @@ -139,9 +141,6 @@ abstract class MultiListFragment : Fragment() { layoutManager = viewManager adapter = viewAdapter } - - // Configure whether to show the folder header - // viewAdapter.folderHeaderEnabled = showFolderHeader() } @Override diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 57b7cec9..75c24988 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -859,7 +859,7 @@ class PlayerFragment : viewAdapter.register( TrackViewBinder( onItemClick = listener, - onContextMenuClick = {_,_ -> true}, + onContextMenuClick = { _, _ -> true }, checkable = false, draggable = true, context = requireContext(), @@ -880,10 +880,9 @@ class PlayerFragment : val from = viewHolder.bindingAdapterPosition val to = target.bindingAdapterPosition - // FIXME: - // Needs to be changed in the playlist as well... - // Move it in the data set - (recyclerView.adapter as BaseAdapter<*>).moveItem(from, to) + // Move it in the data set + mediaPlayerController.moveItemInPlaylist(from, to) + viewAdapter.submitList(mediaPlayerController.playList) return true } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 2050597e..a9377cda 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -370,7 +370,14 @@ class SearchFragment : MultiListFragment(), KoinComponent { @Suppress("LongMethod") override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean { val isArtist = (item is Artist) - val found = EntryListFragment.handleContextMenu(menuItem, item, isArtist, downloadHandler, this) + + val found = EntryListFragment.handleContextMenu( + menuItem, + item, + isArtist, + downloadHandler, + this + ) if (found || item !is DownloadFile) return true diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 89c34c7d..fb97da1b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -46,24 +46,24 @@ 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 timber.log.Timber /** * Displays a group of tracks, eg. the songs of an album, of a playlist etc. * FIXME: Offset when navigating to? */ +@Suppress("TooManyFunctions") open class TrackCollectionFragment : MultiListFragment() { private var albumButtons: View? = null internal var selectButton: ImageView? = null internal var playNowButton: ImageView? = null - internal var playNextButton: ImageView? = null - internal var playLastButton: ImageView? = null + private var playNextButton: ImageView? = null + private var playLastButton: ImageView? = null internal var pinButton: ImageView? = null - internal var unpinButton: ImageView? = null - internal var downloadButton: ImageView? = null - internal var deleteButton: ImageView? = null - internal var moreButton: ImageView? = null + private var unpinButton: ImageView? = null + private var downloadButton: ImageView? = null + private var deleteButton: ImageView? = null + private var moreButton: ImageView? = null private var playAllButtonVisible = false private var shareButtonVisible = false private var playAllButton: MenuItem? = null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt index 5c3663ae..1919c7be 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import kotlinx.coroutines.Dispatchers @@ -16,14 +17,15 @@ import org.koin.core.component.inject import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.Settings /** -* An abstract Model, which can be extended to retrieve a list of items from the API -*/ + * An abstract Model, which can be extended to retrieve a list of items from the API + */ open class GenericListModel(application: Application) : AndroidViewModel(application), KoinComponent { @@ -38,6 +40,8 @@ open class GenericListModel(application: Application) : var currentListIsSortable = true var showHeader = true + val musicFolders: MutableLiveData> = MutableLiveData(listOf()) + @Suppress("UNUSED_PARAMETER") open fun showSelectFolderHeader(args: Bundle?): Boolean { return true @@ -105,8 +109,11 @@ open class GenericListModel(application: Application) : args: Bundle ) { // Update the list of available folders if enabled + // FIXME && refresh ? if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) { - // FIXME + musicFolders.postValue( + musicService.getMusicFolders(refresh) + ) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index aa22751b..073742e1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -369,6 +369,20 @@ class Downloader( checkDownloads() } + fun moveItemInPlaylist(oldPos: Int, newPos: Int) { + val item = playlist[oldPos] + playlist.remove(item) + + if (newPos < oldPos) { + playlist.add(newPos + 1, item) + } else { + playlist.add(newPos - 1, item) + } + + playlistUpdateRevision++ + checkDownloads() + } + @Synchronized fun clearIncomplete() { val iterator = playlist.iterator() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index e3322213..8859a272 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -250,6 +250,11 @@ class MediaPlayerController( mediaPlayerService?.setNextPlaying() } + @Synchronized + fun moveItemInPlaylist(oldPos: Int, newPos: Int) { + downloader.moveItemInPlaylist(oldPos, newPos) + } + @set:Synchronized var repeatMode: RepeatMode get() = Settings.repeatMode diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt deleted file mode 100644 index f090d9d5..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.moire.ultrasonic.util - -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.ItemTouchHelper.DOWN -import androidx.recyclerview.widget.ItemTouchHelper.UP -import androidx.recyclerview.widget.RecyclerView -import org.moire.ultrasonic.adapters.BaseAdapter - -class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { - - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - - val from = viewHolder.bindingAdapterPosition - val to = target.bindingAdapterPosition - - // FIXME: Move it in the data set - (recyclerView.adapter as BaseAdapter<*>).moveItem(from, to) - - return true - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - } -} diff --git a/ultrasonic/src/main/res/navigation/navigation_graph.xml b/ultrasonic/src/main/res/navigation/navigation_graph.xml index 096b5d1a..0ab8f38c 100644 --- a/ultrasonic/src/main/res/navigation/navigation_graph.xml +++ b/ultrasonic/src/main/res/navigation/navigation_graph.xml @@ -25,15 +25,21 @@ android:name="org.moire.ultrasonic.fragment.ArtistListFragment" android:label="@string/music_library.label" > + + Date: Mon, 29 Nov 2021 19:00:28 +0100 Subject: [PATCH 18/33] Fix header glitch --- .../moire/ultrasonic/adapters/BaseAdapter.kt | 2 ++ .../fragment/TrackCollectionFragment.kt | 12 ++++------ .../ultrasonic/model/TrackCollectionModel.kt | 23 ++++++++----------- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index f2a64633..d17f25e2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -17,6 +17,7 @@ import androidx.recyclerview.widget.DiffUtil import com.drakeet.multitype.MultiTypeAdapter import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.util.BoundedTreeSet +import timber.log.Timber /** * The BaseAdapter which extends the MultiTypeAdapter from an external library. @@ -86,6 +87,7 @@ class BaseAdapter : MultiTypeAdapter() { * @param list The new list to be displayed. */ fun submitList(list: List?) { + Timber.v("Received fresh list, size %s", list?.size) mDiffer.submitList(list) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index fb97da1b..fa63d937 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -49,7 +49,7 @@ import org.moire.ultrasonic.util.Util /** * Displays a group of tracks, eg. the songs of an album, of a playlist etc. - * FIXME: Offset when navigating to? + * FIXME: Mixed lists are not handled correctly */ @Suppress("TooManyFunctions") open class TrackCollectionFragment : MultiListFragment() { @@ -93,7 +93,7 @@ open class TrackCollectionFragment : MultiListFragment() { refreshData(true) } - listModel.currentList.observe(viewLifecycleOwner, updateInterfaceWithEntries) + // TODO: remove special casing for songsForGenre listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver) setupButtons(view) @@ -137,9 +137,6 @@ open class TrackCollectionFragment : MultiListFragment() { enableButtons() } ) - - // Loads the data - refreshData(false) } internal open fun setupButtons(view: View) { @@ -450,7 +447,7 @@ open class TrackCollectionFragment : MultiListFragment() { } } - private val updateInterfaceWithEntries = Observer> { + override val defaultObserver: (List) -> Unit = { val entryList: MutableList = it.toMutableList() @@ -513,9 +510,8 @@ open class TrackCollectionFragment : MultiListFragment() { shareButton?.isVisible = shareButtonVisible if (songCount > 0 && listModel.showHeader) { - val name = listModel.currentDirectory.value?.name val intentAlbumName = arguments?.getString(Constants.INTENT_EXTRA_NAME_NAME, "") - val albumHeader = AlbumHeader(it, name ?: intentAlbumName) + val albumHeader = AlbumHeader(it, intentAlbumName) val mixedList: MutableList = mutableListOf(albumHeader) mixedList.addAll(entryList) viewAdapter.submitList(mixedList) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index 3fbbf45a..a3d0203f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -18,16 +18,15 @@ import org.moire.ultrasonic.util.Util /* * Model for retrieving different collections of tracks from the API -* -* TODO: Remove double data keeping in currentList/currentDirectory and use the base model liveData -* For this refactor MusicService to replace MusicDirectories with List or List */ class TrackCollectionModel(application: Application) : GenericListModel(application) { - val currentDirectory: MutableLiveData = MutableLiveData() val currentList: MutableLiveData> = MutableLiveData() val songsForGenre: MutableLiveData = MutableLiveData() + /* + * Especially when dealing with indexes, this method can return Albums, Entries or a mix of both! + */ suspend fun getMusicDirectory( refresh: Boolean, id: String, @@ -39,7 +38,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat val service = MusicServiceFactory.getMusicService() val musicDirectory = service.getMusicDirectory(id, name, refresh) - currentDirectory.postValue(musicDirectory) updateList(musicDirectory) } } @@ -71,7 +69,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat val service = MusicServiceFactory.getMusicService() val musicDirectory: MusicDirectory = service.getAlbum(id, name, refresh) - currentDirectory.postValue(musicDirectory) updateList(musicDirectory) } } @@ -96,8 +93,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } else { musicDirectory = Util.getSongsFromSearchResult(service.getStarred()) } - - currentDirectory.postValue(musicDirectory) + updateList(musicDirectory) } } @@ -108,7 +104,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val videos = service.getVideos(refresh) - currentDirectory.postValue(videos) + if (videos != null) { updateList(videos) } @@ -122,7 +118,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat val musicDirectory = service.getRandomSongs(size) currentListIsSortable = false - currentDirectory.postValue(musicDirectory) + updateList(musicDirectory) } } @@ -133,7 +129,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat val service = MusicServiceFactory.getMusicService() val musicDirectory = service.getPlaylist(playlistId, playlistName) - currentDirectory.postValue(musicDirectory) updateList(musicDirectory) } } @@ -143,7 +138,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val musicDirectory = service.getPodcastEpisodes(podcastChannelId) - currentDirectory.postValue(musicDirectory) + if (musicDirectory != null) { updateList(musicDirectory) } @@ -166,7 +161,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat break } } - currentDirectory.postValue(musicDirectory) + updateList(musicDirectory) } } @@ -175,7 +170,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val musicDirectory = Util.getSongsFromBookmarks(service.getBookmarks()) - currentDirectory.postValue(musicDirectory) + updateList(musicDirectory) } } From aa33d7c882217923e4d74e21ff99d41549acfb3d Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 29 Nov 2021 20:14:11 +0100 Subject: [PATCH 19/33] Cleanup nested functions on OfflineMusicService and make it return the correct MusicDirectory type --- .../moire/ultrasonic/domain/MusicDirectory.kt | 18 +- .../fragment/TrackCollectionFragment.kt | 4 +- .../ultrasonic/model/TrackCollectionModel.kt | 25 +- .../ultrasonic/service/OfflineMusicService.kt | 373 +++++++++--------- 4 files changed, 210 insertions(+), 210 deletions(-) diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt index 805088bd..c8c319d7 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt @@ -34,13 +34,13 @@ class MusicDirectory : ArrayList() { abstract class Child : Identifiable, GenericEntry() { abstract override var id: String - abstract val parent: String? - abstract val isDirectory: Boolean + abstract var parent: String? + abstract var isDirectory: Boolean abstract var album: String? - abstract val title: String? + abstract var title: String? abstract override val name: String? abstract val discNumber: Int? - abstract val coverArt: String? + abstract var coverArt: String? abstract val songCount: Long? abstract val created: Date? abstract var artist: String? @@ -49,7 +49,7 @@ class MusicDirectory : ArrayList() { abstract val year: Int? abstract val genre: String? abstract var starred: Boolean - abstract val path: String? + abstract var path: String? abstract var closeness: Int } @@ -115,12 +115,12 @@ class MusicDirectory : ArrayList() { data class Album( @PrimaryKey override var id: String, - override val parent: String? = null, + override var parent: String? = null, override var album: String? = null, - override val title: String? = null, + override var title: String? = null, override val name: String? = null, override val discNumber: Int = 0, - override val coverArt: String? = null, + override var coverArt: String? = null, override val songCount: Long? = null, override val created: Date? = null, override var artist: String? = null, @@ -132,6 +132,6 @@ class MusicDirectory : ArrayList() { override var path: String? = null, override var closeness: Int = 0, ) : Child() { - override val isDirectory = true + override var isDirectory = true } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index fa63d937..9fa39b9c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -604,12 +604,12 @@ open class TrackCollectionFragment : MultiListFragment() { setTitle(name) if (!isOffline() && Settings.shouldUseId3Tags) { if (isAlbum) { - listModel.getAlbum(refresh, id!!, name, parentId) + listModel.getAlbum(refresh, id!!, name) } else { throw IllegalAccessException("Use AlbumFragment instead!") } } else { - listModel.getMusicDirectory(refresh, id!!, name, parentId) + listModel.getMusicDirectory(refresh, id!!, name) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index a3d0203f..a3eebe3d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -30,8 +30,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat suspend fun getMusicDirectory( refresh: Boolean, id: String, - name: String?, - parentId: String? + name: String? ) { withContext(Dispatchers.IO) { @@ -42,27 +41,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } } - // Given a Music directory "songs" it recursively adds all children to "songs" - @Suppress("unused") - private fun getSongsRecursively( - parent: MusicDirectory, - songs: MutableList - ) { - val service = MusicServiceFactory.getMusicService() - - for (song in parent.getTracks()) { - if (!song.isVideo && !song.isDirectory) { - songs.add(song) - } - } - - for ((id1, _, _, title) in parent.getAlbums()) { - val root: MusicDirectory = service.getMusicDirectory(id1, title, false) - getSongsRecursively(root, songs) - } - } - - suspend fun getAlbum(refresh: Boolean, id: String, name: String?, parentId: String?) { + suspend fun getAlbum(refresh: Boolean, id: String, name: String?) { withContext(Dispatchers.IO) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index 5ad9d53f..7714a0ea 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -43,8 +43,6 @@ import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util import timber.log.Timber -// TODO: There are quite a number of deeply nested and complicated functions in this class.. -// Simplify them :) @Suppress("TooManyFunctions") class OfflineMusicService : MusicService, KoinComponent { private val activeServerProvider: ActiveServerProvider by inject() @@ -94,6 +92,9 @@ class OfflineMusicService : MusicService, KoinComponent { return indexes } + /* + * Especially when dealing with indexes, this method can return Albums, Entries or a mix of both! + */ override fun getMusicDirectory( id: String, name: String?, @@ -109,7 +110,11 @@ class OfflineMusicService : MusicService, KoinComponent { val filename = getName(file) if (filename != null && !seen.contains(filename)) { seen.add(filename) - result.add(createEntry(file, filename)) + if (file.isFile) { + result.add(createEntry(file, filename)) + } else { + result.add(createAlbum(file, filename)) + } } } @@ -481,188 +486,204 @@ class OfflineMusicService : MusicService, KoinComponent { throw OfflineException("getPodcastsChannels isn't available in offline mode") } - companion object { - private val COMPILE = Pattern.compile(" ") - private fun getName(file: File): String? { - var name = file.name - if (file.isDirectory) { - return name - } - if (name.endsWith(".partial") || name.contains(".partial.") || - name == Constants.ALBUM_ART_FILE - ) { - return null - } - name = name.replace(".complete", "") - return FileUtil.getBaseName(name) + private fun getName(file: File): String? { + var name = file.name + if (file.isDirectory) { + return name } - - @Suppress("TooGenericExceptionCaught", "ComplexMethod", "LongMethod", "NestedBlockDepth") - private fun createEntry(file: File, name: String?): MusicDirectory.Child { - val entry = MusicDirectory.Entry(file.path) - entry.isDirectory = file.isDirectory - entry.parent = file.parent - entry.size = file.length() - val root = FileUtil.musicDirectory.path - entry.path = file.path.replaceFirst( - String.format(Locale.ROOT, "^%s/", root).toRegex(), "" - ) - entry.title = name - if (file.isFile) { - var artist: String? = null - var album: String? = null - var title: String? = null - var track: String? = null - var disc: String? = null - var year: String? = null - var genre: String? = null - var duration: String? = null - var hasVideo: String? = null - try { - val mmr = MediaMetadataRetriever() - mmr.setDataSource(file.path) - artist = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST) - album = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM) - title = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE) - track = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER) - disc = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER) - year = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR) - genre = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE) - duration = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) - hasVideo = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO) - mmr.release() - } catch (ignored: Exception) { - } - entry.artist = artist ?: file.parentFile!!.parentFile!!.name - entry.album = album ?: file.parentFile!!.name - if (title != null) { - entry.title = title - } - entry.isVideo = hasVideo != null - Timber.i("Offline Stuff: %s", track) - if (track != null) { - var trackValue = 0 - try { - val slashIndex = track.indexOf('/') - if (slashIndex > 0) { - track = track.substring(0, slashIndex) - } - trackValue = track.toInt() - } catch (ex: Exception) { - Timber.e(ex, "Offline Stuff") - } - Timber.i("Offline Stuff: Setting Track: %d", trackValue) - entry.track = trackValue - } - if (disc != null) { - var discValue = 0 - try { - val slashIndex = disc.indexOf('/') - if (slashIndex > 0) { - disc = disc.substring(0, slashIndex) - } - discValue = disc.toInt() - } catch (ignored: Exception) { - } - entry.discNumber = discValue - } - if (year != null) { - var yearValue = 0 - try { - yearValue = year.toInt() - } catch (ignored: Exception) { - } - entry.year = yearValue - } - if (genre != null) { - entry.genre = genre - } - if (duration != null) { - var durationValue: Long = 0 - try { - durationValue = duration.toLong() - durationValue = TimeUnit.MILLISECONDS.toSeconds(durationValue) - } catch (ignored: Exception) { - } - entry.setDuration(durationValue) - } - } - entry.suffix = FileUtil.getExtension(file.name.replace(".complete", "")) - val albumArt = FileUtil.getAlbumArtFile(entry) - if (albumArt.exists()) { - entry.coverArt = albumArt.path - } - return entry - } - - @Suppress("NestedBlockDepth") - private fun recursiveAlbumSearch( - artistName: String, - file: File, - criteria: SearchCriteria, - albums: MutableList, - songs: MutableList + if (name.endsWith(".partial") || name.contains(".partial.") || + name == Constants.ALBUM_ART_FILE ) { - var closeness: Int - for (albumFile in FileUtil.listMediaFiles(file)) { - if (albumFile.isDirectory) { - val albumName = getName(albumFile) - if (matchCriteria(criteria, albumName).also { closeness = it } > 0) { - val album = createEntry(albumFile, albumName) - album.artist = artistName - album.closeness = closeness - albums.add(album as MusicDirectory.Album) - } - for (songFile in FileUtil.listMediaFiles(albumFile)) { - val songName = getName(songFile) - if (songFile.isDirectory) { - recursiveAlbumSearch(artistName, songFile, criteria, albums, songs) - } else if (matchCriteria(criteria, songName).also { closeness = it } > 0) { - val song = createEntry(albumFile, songName) - song.artist = artistName - song.album = albumName - song.closeness = closeness - songs.add(song as MusicDirectory.Entry) - } - } - } else { - val songName = getName(albumFile) - if (matchCriteria(criteria, songName).also { closeness = it } > 0) { + return null + } + name = name.replace(".complete", "") + return FileUtil.getBaseName(name) + } + + private fun createEntry(file: File, name: String?): MusicDirectory.Entry { + val entry = MusicDirectory.Entry(file.path) + entry.populateWithDataFrom(file, name) + return entry + } + + private fun createAlbum(file: File, name: String?): MusicDirectory.Album { + val album = MusicDirectory.Album(file.path) + album.populateWithDataFrom(file, name) + return album + } + + + /* + * Extracts some basic data from a File object and applies it to an Album or Entry + */ + private fun MusicDirectory.Child.populateWithDataFrom(file: File, name: String?) { + isDirectory = file.isDirectory + parent = file.parent + val root = FileUtil.musicDirectory.path + path = file.path.replaceFirst( + String.format(Locale.ROOT, "^%s/", root).toRegex(), "" + ) + title = name + + val albumArt = FileUtil.getAlbumArtFile(file) + if (albumArt.exists()) { + coverArt = albumArt.path + } + } + + + /* + * More extensive variant of Child.populateWithDataFrom(), which also parses the ID3 tags of + * a given track file. + */ + private fun MusicDirectory.Entry.populateWithDataFrom(file: File, name: String?) { + (this as MusicDirectory.Child).populateWithDataFrom(file, name) + + val meta = RawMetadata(null) + + try { + val mmr = MediaMetadataRetriever() + mmr.setDataSource(file.path) + meta.artist = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST) + meta.album = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM) + meta.title = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE) + meta.track = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER) + meta.disc = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER) + meta.year = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR) + meta.genre = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE) + meta.duration = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + meta.hasVideo = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO) + mmr.release() + } catch (ignored: Exception) { + } + + artist = meta.artist ?: file.parentFile!!.parentFile!!.name + album = meta.album ?: file.parentFile!!.name + title = meta.title?: title + isVideo = meta.hasVideo != null + track = parseSlashedNumber(meta.track) + discNumber = parseSlashedNumber(meta.disc) + year = meta.year?.toIntOrNull() + genre = meta.genre + duration = parseDuration(meta.duration) + size = file.length() + suffix = FileUtil.getExtension(file.name.replace(".complete", "")) + } + + /* + * Parses a number from a string in the format of 05/21, + * where the first number is the track number + * and the second the number of total tracks + */ + private fun parseSlashedNumber(string: String?): Int? { + if (string == null) return null + + val slashIndex = string.indexOf('/') + if (slashIndex > 0) + return string.substring(0, slashIndex).toIntOrNull() + else + return string.toIntOrNull() + } + + /* + * Parses a duration from a String + */ + private fun parseDuration(string: String?): Int? { + if (string == null) return null + + val duration: Long? = string.toLongOrNull() + + if (duration != null) + return TimeUnit.MILLISECONDS.toSeconds(duration).toInt() + else + return null + } + + // TODO: Simplify this deeply nested and complicated function + @Suppress("NestedBlockDepth") + private fun recursiveAlbumSearch( + artistName: String, + file: File, + criteria: SearchCriteria, + albums: MutableList, + songs: MutableList + ) { + var closeness: Int + for (albumFile in FileUtil.listMediaFiles(file)) { + if (albumFile.isDirectory) { + val albumName = getName(albumFile) + if (matchCriteria(criteria, albumName).also { closeness = it } > 0) { + val album = createAlbum(albumFile, albumName) + album.artist = artistName + album.closeness = closeness + albums.add(album) + } + for (songFile in FileUtil.listMediaFiles(albumFile)) { + val songName = getName(songFile) + if (songFile.isDirectory) { + recursiveAlbumSearch(artistName, songFile, criteria, albums, songs) + } else if (matchCriteria(criteria, songName).also { closeness = it } > 0) { val song = createEntry(albumFile, songName) song.artist = artistName - song.album = songName + song.album = albumName song.closeness = closeness - songs.add(song as MusicDirectory.Entry) + songs.add(song) } } - } - } - - private fun matchCriteria(criteria: SearchCriteria, name: String?): Int { - val query = criteria.query.lowercase(Locale.ROOT) - val queryParts = COMPILE.split(query) - val nameParts = COMPILE.split( - name!!.lowercase(Locale.ROOT) - ) - var closeness = 0 - for (queryPart in queryParts) { - for (namePart in nameParts) { - if (namePart == queryPart) { - closeness++ - } - } - } - return closeness - } - - private fun listFilesRecursively(parent: File, children: MutableList) { - for (file in FileUtil.listMediaFiles(parent)) { - if (file.isFile) { - children.add(file) - } else { - listFilesRecursively(file, children) + } else { + val songName = getName(albumFile) + if (matchCriteria(criteria, songName).also { closeness = it } > 0) { + val song = createEntry(albumFile, songName) + song.artist = artistName + song.album = songName + song.closeness = closeness + songs.add(song) } } } } + + private fun matchCriteria(criteria: SearchCriteria, name: String?): Int { + val query = criteria.query.lowercase(Locale.ROOT) + val queryParts = COMPILE.split(query) + val nameParts = COMPILE.split( + name!!.lowercase(Locale.ROOT) + ) + var closeness = 0 + for (queryPart in queryParts) { + for (namePart in nameParts) { + if (namePart == queryPart) { + closeness++ + } + } + } + return closeness + } + + + private fun listFilesRecursively(parent: File, children: MutableList) { + for (file in FileUtil.listMediaFiles(parent)) { + if (file.isFile) { + children.add(file) + } else { + listFilesRecursively(file, children) + } + } + } + + data class RawMetadata(val id: String?) { + var artist: String? = null + var album: String? = null + var title: String? = null + var track: String? = null + var disc: String? = null + var year: String? = null + var genre: String? = null + var duration: String? = null + var hasVideo: String? = null + } + + companion object { + private val COMPILE = Pattern.compile(" ") + } } From bdac092eff4e8f2d21c794b8e9dc2ca0661c53aa Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 30 Nov 2021 00:46:48 +0100 Subject: [PATCH 20/33] Make SearchResults expandable, finish music folder support, change Service interface of AlbumList to return listOf(Album) --- .../moire/ultrasonic/domain/MusicDirectory.kt | 4 +- .../subsonic/response/GetAlbumListResponse.kt | 6 +- .../util/EntryByDiscAndTrackComparator.java | 72 ---------- .../moire/ultrasonic/adapters}/AlbumHeader.kt | 8 +- .../ultrasonic/adapters/DividerBinder.kt | 1 + .../ultrasonic/adapters/HeaderViewBinder.kt | 1 - .../ultrasonic/adapters/MoreButtonBinder.kt | 50 +++++++ .../ultrasonic/domain/APIAlbumConverter.kt | 3 +- .../ultrasonic/fragment/AlbumListFragment.kt | 40 +----- .../ultrasonic/fragment/ArtistListFragment.kt | 10 +- .../ultrasonic/fragment/BookmarksFragment.kt | 3 +- .../ultrasonic/fragment/DownloadsFragment.kt | 2 +- .../ultrasonic/fragment/EntryListFragment.kt | 40 +++++- .../ultrasonic/fragment/MultiListFragment.kt | 4 +- .../ultrasonic/fragment/SearchFragment.kt | 129 ++++++------------ .../fragment/TrackCollectionFragment.kt | 51 ++++--- .../moire/ultrasonic/model/AlbumListModel.kt | 30 ++-- .../ultrasonic/model/GenericListModel.kt | 16 +-- .../moire/ultrasonic/model/SearchListModel.kt | 13 +- .../ultrasonic/model/TrackCollectionModel.kt | 16 +-- .../service/AutoMediaBrowserService.kt | 2 +- .../ultrasonic/service/CachedMusicService.kt | 4 +- .../moire/ultrasonic/service/MusicService.kt | 9 +- .../ultrasonic/service/OfflineMusicService.kt | 27 ++-- .../ultrasonic/service/RESTMusicService.kt | 14 +- .../util/EntryByDiscAndTrackComparator.kt | 50 +++++++ .../main/res/layout/list_item_more_button.xml | 16 +++ .../src/main/res/layout/search_buttons.xml | 39 ------ 28 files changed, 319 insertions(+), 341 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.java rename ultrasonic/src/main/{java/org/moire/ultrasonic/util => kotlin/org/moire/ultrasonic/adapters}/AlbumHeader.kt (91%) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MoreButtonBinder.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.kt create mode 100644 ultrasonic/src/main/res/layout/list_item_more_button.xml delete mode 100644 ultrasonic/src/main/res/layout/search_buttons.xml diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt index c8c319d7..b409acab 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt @@ -51,6 +51,7 @@ class MusicDirectory : ArrayList() { abstract var starred: Boolean abstract var path: String? abstract var closeness: Int + abstract var isVideo: Boolean } // TODO: Rename to Track @@ -77,7 +78,7 @@ class MusicDirectory : ArrayList() { override var duration: Int? = null, var bitRate: Int? = null, override var path: String? = null, - var isVideo: Boolean = false, + override var isVideo: Boolean = false, override var starred: Boolean = false, override var discNumber: Int? = null, var type: String? = null, @@ -133,5 +134,6 @@ class MusicDirectory : ArrayList() { override var closeness: Int = 0, ) : Child() { override var isDirectory = true + override var isVideo = false } } diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt index 8e3ca708..81c6be5b 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt @@ -3,7 +3,7 @@ package org.moire.ultrasonic.api.subsonic.response import com.fasterxml.jackson.annotation.JsonProperty import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicError -import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild +import org.moire.ultrasonic.api.subsonic.models.Album class GetAlbumListResponse( status: Status, @@ -12,10 +12,10 @@ class GetAlbumListResponse( ) : SubsonicResponse(status, version, error) { @JsonProperty("albumList") private val albumWrapper = AlbumWrapper() - val albumList: List + val albumList: List get() = albumWrapper.albumList } private class AlbumWrapper( - @JsonProperty("album") val albumList: List = emptyList() + @JsonProperty("album") val albumList: List = emptyList() ) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.java deleted file mode 100644 index cbf91c91..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.moire.ultrasonic.util; - -import org.moire.ultrasonic.domain.MusicDirectory; - -import java.io.Serializable; -import java.util.Comparator; - -public class EntryByDiscAndTrackComparator implements Comparator, Serializable -{ - private static final long serialVersionUID = 5540441864560835223L; - - @Override - public int compare(MusicDirectory.Entry x, MusicDirectory.Entry y) - { - Integer discX = x.getDiscNumber(); - Integer discY = y.getDiscNumber(); - Integer trackX = x.getTrack(); - Integer trackY = y.getTrack(); - String albumX = x.getAlbum(); - String albumY = y.getAlbum(); - String pathX = x.getPath(); - String pathY = y.getPath(); - - int albumComparison = compare(albumX, albumY); - - if (albumComparison != 0) - { - return albumComparison; - } - - int discComparison = compare(discX == null ? 0 : discX, discY == null ? 0 : discY); - - if (discComparison != 0) - { - return discComparison; - } - - int trackComparison = compare(trackX == null ? 0 : trackX, trackY == null ? 0 : trackY); - - if (trackComparison != 0) - { - return trackComparison; - } - - return compare(pathX == null ? "" : pathX, pathY == null ? "" : pathY); - } - - private static int compare(long a, long b) - { - return Long.compare(a, b); - } - - private static int compare(String a, String b) - { - if (a == null && b == null) - { - return 0; - } - - if (a == null) - { - return -1; - } - - if (b == null) - { - return 1; - } - - return a.compareTo(b); - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumHeader.kt similarity index 91% rename from ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumHeader.kt index c049d49c..978ead6f 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumHeader.kt @@ -1,4 +1,4 @@ -package org.moire.ultrasonic.util +package org.moire.ultrasonic.adapters import java.util.HashSet import org.moire.ultrasonic.domain.Identifiable @@ -7,7 +7,7 @@ import org.moire.ultrasonic.util.Settings.shouldUseFolderForArtistName import org.moire.ultrasonic.util.Util.getGrandparent class AlbumHeader( - var entries: List, + var entries: List, var name: String? ) : Identifiable { var isAllVideo: Boolean @@ -35,7 +35,7 @@ class AlbumHeader( val years: Set get() = _years - private fun processGrandParents(entry: MusicDirectory.Entry) { + private fun processGrandParents(entry: MusicDirectory.Child) { val grandParent = getGrandparent(entry.path) if (grandParent != null) { _grandParents.add(grandParent) @@ -43,7 +43,7 @@ class AlbumHeader( } @Suppress("NestedBlockDepth") - private fun processEntries(list: List) { + private fun processEntries(list: List) { entries = list childCount = entries.size for (entry in entries) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt index 679839a7..b4f4627c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt @@ -16,6 +16,7 @@ class DividerBinder : ItemViewBinder() { + + // Set our layout files + val layout = R.layout.list_item_more_button + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: MoreButton) { + holder.itemView.setOnClickListener { + item.onClick() + } + } + + override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup + ): RecyclerView.ViewHolder { + return ViewHolder(inflater.inflate(layout, parent, false)) + } + + // ViewHolder class + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) + + // Class to store our data into + data class MoreButton( + val stringId: Int, + val onClick: (() -> Unit) + ): Identifiable { + + override val id: String + get() = stringId.toString() + override val longId: Long + get() = stringId.toLong() + + override fun compareTo(other: Identifiable): Int = longId.compareTo(other.longId) + } + +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt index 6fc540a2..acccda31 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt @@ -7,7 +7,8 @@ import org.moire.ultrasonic.api.subsonic.models.Album fun Album.toDomainEntity(): MusicDirectory.Album = MusicDirectory.Album( id = this@toDomainEntity.id, - title = this@toDomainEntity.name, + title = this@toDomainEntity.title, + album = this@toDomainEntity.album, coverArt = this@toDomainEntity.coverArt, artist = this@toDomainEntity.artist, artistId = this@toDomainEntity.artistId, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 30a0a601..e719e8f8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -24,7 +24,6 @@ import org.moire.ultrasonic.util.Constants /** * Displays a list of Albums from the media library - * FIXME: Add music folder support */ class AlbumListFragment : EntryListFragment() { @@ -41,10 +40,10 @@ class AlbumListFragment : EntryListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(args: Bundle?): LiveData> { + override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { if (args == null) throw IllegalArgumentException("Required arguments are missing") - val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) + val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) || refresh val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND) return listModel.getAlbumList(refresh or append, refreshListView!!, args) @@ -87,39 +86,4 @@ class AlbumListFragment : EntryListFragment() { bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent) findNavController().navigate(R.id.trackCollectionFragment, bundle) } - - /** - * What to do when the list has changed - */ - override val defaultObserver: (List) -> Unit = { - emptyView.isVisible = it.isEmpty() - - if (showFolderHeader()) { - @Suppress("UNCHECKED_CAST") - val list = it as MutableList - list.add(0, folderHeader) - } else { - viewAdapter.submitList(it) - } - } - - /** - * Get a folder header and update it on changes - */ - private val folderHeader: FolderSelectorBinder.FolderHeader by lazy { - val header = FolderSelectorBinder.FolderHeader( - listModel.musicFolders.value!!, - listModel.activeServer.musicFolderId - ) - - listModel.musicFolders.observe( - viewLifecycleOwner, - { - header.folders = it - viewAdapter.notifyItemChanged(0) - } - ) - - header - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 1ec67ac3..73346de6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -2,19 +2,25 @@ package org.moire.ultrasonic.fragment import android.os.Bundle import android.view.View +import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import androidx.navigation.fragment.findNavController import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.ArtistRowBinder +import org.moire.ultrasonic.adapters.FolderSelectorBinder import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.ArtistOrIndex +import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Index +import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.model.ArtistListModel import org.moire.ultrasonic.util.Constants /** * Displays the list of Artists from the media library + * + * FIXME: FOLDER HEADER NOT POPULATED ON FIST LOAD */ class ArtistListFragment : EntryListFragment() { @@ -31,8 +37,8 @@ class ArtistListFragment : EntryListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(args: Bundle?): LiveData> { - val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false + override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { + val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false || refresh return listModel.getItems(refresh, refreshListView!!) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt index 9fdcfa5c..8f575ce7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -24,7 +24,6 @@ import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle * audio books etc. * * Therefore this fragment allows only for singular selection and playback. - * */ class BookmarksFragment : TrackCollectionFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -35,7 +34,7 @@ class BookmarksFragment : TrackCollectionFragment() { viewAdapter.selectionType = BaseAdapter.SelectionType.SINGLE } - override fun getLiveData(args: Bundle?): LiveData> { + override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { listModel.viewModelScope.launch(handler) { refreshListView?.isRefreshing = true listModel.getBookmarks() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index 1ad3c658..86847435 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -41,7 +41,7 @@ class DownloadsFragment : MultiListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(args: Bundle?): LiveData> { + override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { return listModel.getList() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 44dc1aef..00726af2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -3,11 +3,13 @@ package org.moire.ultrasonic.fragment import android.os.Bundle import android.view.MenuItem import android.view.View +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.FolderSelectorBinder import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.domain.GenericEntry import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.service.RxBus @@ -48,15 +50,12 @@ abstract class EntryListFragment : MultiListFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // FIXME: What to do when the user has modified the folder filter RxBus.musicFolderChangedEventObservable.subscribe { if (!listModel.isOffline()) { val currentSetting = listModel.activeServer currentSetting.musicFolderId = it serverSettingsModel.updateItem(currentSetting) } - // FIXME: Needed? - viewAdapter.notifyDataSetChanged() listModel.refresh(refreshListView!!, arguments) } @@ -65,6 +64,41 @@ abstract class EntryListFragment : MultiListFragment() { ) } + /** + * What to do when the list has changed + */ + override val defaultObserver: (List) -> Unit = { + emptyView.isVisible = it.isEmpty() + + if (showFolderHeader()) { + val list = mutableListOf(folderHeader) + list.addAll(it) + viewAdapter.submitList(list) + } else { + viewAdapter.submitList(it) + } + } + + /** + * Get a folder header and update it on changes + */ + private val folderHeader: FolderSelectorBinder.FolderHeader by lazy { + val header = FolderSelectorBinder.FolderHeader( + listModel.musicFolders.value!!, + listModel.activeServer.musicFolderId + ) + + listModel.musicFolders.observe( + viewLifecycleOwner, + { + header.folders = it + viewAdapter.notifyItemChanged(0) + } + ) + + header + } + companion object { @Suppress("LongMethod") internal fun handleContextMenu( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index 5588cebb..d0230a65 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -72,7 +72,7 @@ abstract class MultiListFragment : Fragment() { /** * The central function to pass a query to the model and return a LiveData object */ - open fun getLiveData(args: Bundle? = null): LiveData> { + open fun getLiveData(args: Bundle? = null, refresh: Boolean = false): LiveData> { return MutableLiveData() } @@ -123,7 +123,7 @@ abstract class MultiListFragment : Fragment() { } // Populate the LiveData. This starts an API request in most cases - liveDataItems = getLiveData(arguments) + liveDataItems = getLiveData(arguments, true) // Link view to display text if the list is empty emptyView = view.findViewById(emptyViewId) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index a9377cda..ded120e4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -22,6 +22,8 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.AlbumRowBinder import org.moire.ultrasonic.adapters.ArtistRowBinder import org.moire.ultrasonic.adapters.DividerBinder +import org.moire.ultrasonic.adapters.MoreButtonBinder +import org.moire.ultrasonic.adapters.MoreButtonBinder.MoreButton import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Identifiable @@ -44,18 +46,10 @@ import timber.log.Timber /** * Initiates a search on the media library and displays the results - * - * FIXME: Handle context click on song + * FIXME: Artist click, display */ class SearchFragment : MultiListFragment(), KoinComponent { - private var moreArtistsButton: View? = null - private var moreAlbumsButton: View? = null - private var moreSongsButton: View? = null private var searchResult: SearchResult? = null - private var artistAdapter: ArtistAdapter? = null - private var moreArtistsAdapter: ListAdapter? = null - private var moreAlbumsAdapter: ListAdapter? = null - private var moreSongsAdapter: ListAdapter? = null private var searchRefresh: SwipeRefreshLayout? = null private val mediaPlayerController: MediaPlayerController by inject() @@ -75,40 +69,20 @@ class SearchFragment : MultiListFragment(), KoinComponent { setTitle(this, R.string.search_title) setHasOptionsMenu(true) - val buttons = LayoutInflater.from(context).inflate( - R.layout.search_buttons, - listView, false - ) - - if (buttons != null) { - moreArtistsButton = buttons.findViewById(R.id.search_more_artists) - moreAlbumsButton = buttons.findViewById(R.id.search_more_albums) - moreSongsButton = buttons.findViewById(R.id.search_more_songs) - } - listModel.searchResult.observe( viewLifecycleOwner, { - if (it != null) populateList(it) + if (it != null) { + // Shorten the display initially + searchResult = it + populateList(listModel.trimResultLength(it)) + } } ) searchRefresh = view.findViewById(R.id.swipe_refresh_view) searchRefresh!!.isEnabled = false -// list.setOnItemClickListener(OnItemClickListener { parent: AdapterView<*>, view1: View, position: Int, id: Long -> -// if (view1 === moreArtistsButton) { -// expandArtists() -// } else if (view1 === moreAlbumsButton) { -// expandAlbums() -// } else if (view1 === moreSongsButton) { -// expandSongs() -// } else { -// val item = parent.getItemAtPosition(position) -// -// } -// }) - registerForContextMenu(listView!!) // Register our data binders @@ -147,6 +121,10 @@ class SearchFragment : MultiListFragment(), KoinComponent { DividerBinder() ) + viewAdapter.register( + MoreButtonBinder() + ) + // Fragment was started with a query (e.g. from voice search), try to execute search right away val arguments = arguments if (arguments != null) { @@ -229,45 +207,44 @@ class SearchFragment : MultiListFragment(), KoinComponent { } private fun search(query: String, autoplay: Boolean) { - // FIXME support autoplay listModel.viewModelScope.launch(CommunicationError.getHandler(context)) { refreshListView?.isRefreshing = true listModel.search(query) refreshListView?.isRefreshing = false + }.invokeOnCompletion { + if (it == null && autoplay) { + autoplay() + } } } private fun populateList(result: SearchResult) { - val searchResult = listModel.trimResultLength(result) - val list = mutableListOf() - val artists = searchResult.artists + val artists = result.artists if (artists.isNotEmpty()) { list.add(DividerBinder.Divider(R.string.search_artists)) list.addAll(artists) - if (artists.size > DEFAULT_ARTISTS) { - // FIXME - // list.add((moreArtistsButton, true) + if (searchResult!!.artists.size > artists.size) { + list.add(MoreButton(0, ::expandArtists)) } } - val albums = searchResult.albums + val albums = result.albums if (albums.isNotEmpty()) { list.add(DividerBinder.Divider(R.string.search_albums)) list.addAll(albums) - // mergeAdapter!!.addAdapter(albumAdapter) -// if (albums.size > DEFAULT_ALBUMS) { -// moreAlbumsAdapter = mergeAdapter!!.addView(moreAlbumsButton, true) -// } + if (searchResult!!.albums.size > albums.size) { + list.add(MoreButton(1, ::expandAlbums)) + } } - val songs = searchResult.songs + val songs = result.songs if (songs.isNotEmpty()) { list.add(DividerBinder.Divider(R.string.search_songs)) list.addAll(songs) -// if (songs.size > DEFAULT_SONGS) { -// moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true) -// } + if (searchResult!!.songs.size > songs.size) { + list.add(MoreButton(2, ::expandSongs)) + } } // Show/hide the empty text view @@ -276,35 +253,17 @@ class SearchFragment : MultiListFragment(), KoinComponent { viewAdapter.submitList(list) } -// private fun expandArtists() { -// artistAdapter!!.clear() -// for (artist in searchResult!!.artists) { -// artistAdapter!!.add(artist) -// } -// artistAdapter!!.notifyDataSetChanged() -// mergeAdapter!!.removeAdapter(moreArtistsAdapter) -// mergeAdapter!!.notifyDataSetChanged() -// } -// -// private fun expandAlbums() { -// albumAdapter!!.clear() -// for (album in searchResult!!.albums) { -// albumAdapter!!.add(album) -// } -// albumAdapter!!.notifyDataSetChanged() -// mergeAdapter!!.removeAdapter(moreAlbumsAdapter) -// mergeAdapter!!.notifyDataSetChanged() -// } -// -// private fun expandSongs() { -// songAdapter!!.clear() -// for (song in searchResult!!.songs) { -// songAdapter!!.add(song) -// } -// songAdapter!!.notifyDataSetChanged() -// mergeAdapter!!.removeAdapter(moreSongsAdapter) -// mergeAdapter!!.notifyDataSetChanged() -// } + private fun expandArtists() { + populateList(listModel.trimResultLength(searchResult!!, maxArtists = Int.MAX_VALUE)) + } + + private fun expandAlbums() { + populateList(listModel.trimResultLength(searchResult!!, maxAlbums = Int.MAX_VALUE)) + } + + private fun expandSongs() { + populateList(listModel.trimResultLength(searchResult!!, maxSongs = Int.MAX_VALUE)) + } private fun onArtistSelected(artist: Artist) { val bundle = Bundle() @@ -343,12 +302,6 @@ class SearchFragment : MultiListFragment(), KoinComponent { } } - companion object { - var DEFAULT_ARTISTS = Settings.defaultArtists - var DEFAULT_ALBUMS = Settings.defaultAlbums - var DEFAULT_SONGS = Settings.defaultSongs - } - override fun onItemClick(item: Identifiable) { when (item) { is Artist -> { @@ -464,4 +417,10 @@ class SearchFragment : MultiListFragment(), KoinComponent { return true } + + companion object { + var DEFAULT_ARTISTS = Settings.defaultArtists + var DEFAULT_ALBUMS = Settings.defaultAlbums + var DEFAULT_SONGS = Settings.defaultSongs + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 9fa39b9c..5cd254a5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -28,6 +28,8 @@ 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.AlbumHeader +import org.moire.ultrasonic.adapters.AlbumRowBinder import org.moire.ultrasonic.adapters.HeaderViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline @@ -39,7 +41,6 @@ import org.moire.ultrasonic.service.MediaPlayerController 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 import org.moire.ultrasonic.util.Constants @@ -48,11 +49,16 @@ import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util /** + * * Displays a group of tracks, eg. the songs of an album, of a playlist etc. - * FIXME: Mixed lists are not handled correctly + * + * In most cases the data should be just a list of Entries, but there are some cases + * where the list can contain Albums as well. This happens especially when having ID3 tags disabled, + * or using Offline mode, both in which Indexes instead of Artists are being used. + * */ @Suppress("TooManyFunctions") -open class TrackCollectionFragment : MultiListFragment() { +open class TrackCollectionFragment : MultiListFragment() { private var albumButtons: View? = null internal var selectButton: ImageView? = null @@ -128,6 +134,15 @@ open class TrackCollectionFragment : MultiListFragment() { ) ) + viewAdapter.register( + AlbumRowBinder( + { entry -> onItemClick(entry) }, + { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, + imageLoaderProvider.getImageLoader(), + context = requireContext() + ) + ) + enableButtons() // Update the buttons when the selection has changed @@ -447,9 +462,9 @@ open class TrackCollectionFragment : MultiListFragment() { } } - override val defaultObserver: (List) -> Unit = { + override val defaultObserver: (List) -> Unit = { - val entryList: MutableList = it.toMutableList() + val entryList: MutableList = it.toMutableList() if (listModel.currentListIsSortable && Settings.shouldSortByDisc) { Collections.sort(entryList, EntryByDiscAndTrackComparator()) @@ -470,7 +485,7 @@ open class TrackCollectionFragment : MultiListFragment() { val listSize = arguments?.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) ?: 0 // Hide select button for video lists and singular selection lists - selectButton!!.isVisible = (!allVideos && viewAdapter.hasMultipleSelection()) + selectButton!!.isVisible = !allVideos && viewAdapter.hasMultipleSelection() && songCount > 0 if (songCount > 0) { if (listSize == 0 || songCount < listSize) { @@ -550,12 +565,11 @@ open class TrackCollectionFragment : MultiListFragment() { } @Suppress("LongMethod") - override fun getLiveData(args: Bundle?): LiveData> { + override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { 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 @@ -574,7 +588,7 @@ open class TrackCollectionFragment : MultiListFragment() { val albumListOffset = args.getInt( Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0 ) - val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true) + val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true) || refresh listModel.viewModelScope.launch(handler) { refreshListView?.isRefreshing = true @@ -621,7 +635,7 @@ open class TrackCollectionFragment : MultiListFragment() { @Suppress("LongMethod") override fun onContextMenuItemSelected( menuItem: MenuItem, - item: MusicDirectory.Entry + item: MusicDirectory.Child ): Boolean { val entryId = item.id @@ -673,13 +687,12 @@ open class TrackCollectionFragment : MultiListFragment() { playAll() } R.id.menu_item_share -> { - val entries: MutableList = ArrayList(1) - entries.add(item) - shareHandler.createShare( - this, entries, refreshListView, - cancellationToken!! - ) - return true + if (item is MusicDirectory.Entry) { + shareHandler.createShare( + this, listOf(item), refreshListView, + cancellationToken!! + ) + } } else -> { return super.onContextItemSelected(menuItem) @@ -688,7 +701,7 @@ open class TrackCollectionFragment : MultiListFragment() { return true } - override fun onItemClick(item: MusicDirectory.Entry) { + override fun onItemClick(item: MusicDirectory.Child) { when { item.isDirectory -> { val bundle = Bundle() @@ -701,7 +714,7 @@ open class TrackCollectionFragment : MultiListFragment() { bundle ) } - item.isVideo -> { + item is MusicDirectory.Entry && item.isVideo -> { VideoPlayer.playVideo(requireContext(), item) } else -> { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt index 7ac20f1a..0b935e36 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -33,7 +33,12 @@ class AlbumListModel(application: Application) : GenericListModel(application) { return list } - fun getAlbumsOfArtist(musicService: MusicService, refresh: Boolean, id: String, name: String?) { + private fun getAlbumsOfArtist( + musicService: MusicService, + refresh: Boolean, + id: String, + name: String? + ) { list.postValue(musicService.getArtist(id, name, refresh)) } @@ -51,7 +56,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) { var offset = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND, false) - val musicDirectory: MusicDirectory + val musicDirectory: List val musicFolderId = if (showSelectFolderHeader(args)) { activeServerProvider.getActiveServer().musicFolderId } else { @@ -72,10 +77,11 @@ class AlbumListModel(application: Application) : GenericListModel(application) { } if (useId3Tags) { - musicDirectory = musicService.getAlbumList2( - albumListType, size, - offset, musicFolderId - ) + musicDirectory = + musicService.getAlbumList2( + albumListType, size, + offset, musicFolderId + ) } else { musicDirectory = musicService.getAlbumList( albumListType, size, @@ -85,15 +91,13 @@ class AlbumListModel(application: Application) : GenericListModel(application) { currentListIsSortable = isCollectionSortable(albumListType) - // TODO: Change signature of musicService.getAlbumList to return a List - @Suppress("UNCHECKED_CAST") if (append && list.value != null) { - val list = ArrayList() - list.addAll(this.list.value!!) - list.addAll(musicDirectory.getChildren()) - this.list.postValue(list as List) + val newList = ArrayList() + newList.addAll(list.value!!) + newList.addAll(musicDirectory) + this.list.postValue(newList) } else { - list.postValue(musicDirectory.getChildren() as List) + list.postValue(musicDirectory) } loadedUntil = offset diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt index 1919c7be..c03880fe 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt @@ -16,7 +16,6 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting -import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicServiceFactory @@ -44,7 +43,7 @@ open class GenericListModel(application: Application) : @Suppress("UNUSED_PARAMETER") open fun showSelectFolderHeader(args: Bundle?): Boolean { - return true + return false } /** @@ -109,20 +108,11 @@ open class GenericListModel(application: Application) : args: Bundle ) { // Update the list of available folders if enabled - // FIXME && refresh ? - if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) { + @Suppress("ComplexCondition") + if (showSelectFolderHeader(args) && !isOffline && !useId3Tags && refresh) { musicFolders.postValue( musicService.getMusicFolders(refresh) ) } } - - /** - * Some shared helper functions - */ - - // Returns true if the directory contains only folders - internal fun hasOnlyFolders(musicDirectory: MusicDirectory) = - musicDirectory.getChildren(includeDirs = true, includeFiles = false).size == - musicDirectory.getChildren(includeDirs = true, includeFiles = true).size } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt index a5907352..252c48cb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt @@ -40,11 +40,16 @@ class SearchListModel(application: Application) : GenericListModel(application) } } - fun trimResultLength(result: SearchResult): SearchResult { + fun trimResultLength( + result: SearchResult, + maxArtists: Int = SearchFragment.DEFAULT_ARTISTS, + maxAlbums: Int = SearchFragment.DEFAULT_ALBUMS, + maxSongs: Int = SearchFragment.DEFAULT_SONGS + ): SearchResult { return SearchResult( - artists = result.artists.take(SearchFragment.DEFAULT_ARTISTS), - albums = result.albums.take(SearchFragment.DEFAULT_ALBUMS), - songs = result.songs.take(SearchFragment.DEFAULT_SONGS) + artists = result.artists.take(maxArtists), + albums = result.albums.take(maxAlbums), + songs = result.songs.take(maxSongs) ) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index a3eebe3d..3b658ae6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -21,7 +21,7 @@ import org.moire.ultrasonic.util.Util */ class TrackCollectionModel(application: Application) : GenericListModel(application) { - val currentList: MutableLiveData> = MutableLiveData() + val currentList: MutableLiveData> = MutableLiveData() val songsForGenre: MutableLiveData = MutableLiveData() /* @@ -72,7 +72,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } else { musicDirectory = Util.getSongsFromSearchResult(service.getStarred()) } - + updateList(musicDirectory) } } @@ -83,7 +83,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val videos = service.getVideos(refresh) - + if (videos != null) { updateList(videos) } @@ -97,7 +97,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat val musicDirectory = service.getRandomSongs(size) currentListIsSortable = false - + updateList(musicDirectory) } } @@ -117,7 +117,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val musicDirectory = service.getPodcastEpisodes(podcastChannelId) - + if (musicDirectory != null) { updateList(musicDirectory) } @@ -140,7 +140,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat break } } - + updateList(musicDirectory) } } @@ -149,12 +149,12 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val musicDirectory = Util.getSongsFromBookmarks(service.getBookmarks()) - + updateList(musicDirectory) } } private fun updateList(root: MusicDirectory) { - currentList.postValue(root.getTracks()) + currentList.postValue(root.getChildren()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt index a8c8b91c..02f897fc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt @@ -575,7 +575,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - albums?.getChildren()?.map { album -> + albums?.map { album -> mediaItems.add( album.title ?: "", listOf(MEDIA_ALBUM_ITEM, album.id, album.name) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index bca7aa51..7ebbdd11 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -249,7 +249,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, size: Int, offset: Int, musicFolderId: String? - ): MusicDirectory { + ): List { return musicService.getAlbumList(type, size, offset, musicFolderId) } @@ -259,7 +259,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, size: Int, offset: Int, musicFolderId: String? - ): MusicDirectory { + ): List { return musicService.getAlbumList2(type, size, offset, musicFolderId) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index 5d78644f..410558b8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -90,7 +90,12 @@ interface MusicService { fun scrobble(id: String, submission: Boolean) @Throws(Exception::class) - fun getAlbumList(type: String, size: Int, offset: Int, musicFolderId: String?): MusicDirectory + fun getAlbumList( + type: String, + size: Int, + offset: Int, + musicFolderId: String? + ): List @Throws(Exception::class) fun getAlbumList2( @@ -98,7 +103,7 @@ interface MusicService { size: Int, offset: Int, musicFolderId: String? - ): MusicDirectory + ): List @Throws(Exception::class) fun getRandomSongs(size: Int): MusicDirectory diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index 7714a0ea..fad431f7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -296,10 +296,20 @@ class OfflineMusicService : MusicService, KoinComponent { size: Int, offset: Int, musicFolderId: String? - ): MusicDirectory { + ): List { throw OfflineException("Album lists not available in offline mode") } + @Throws(OfflineException::class) + override fun getAlbumList2( + type: String, + size: Int, + offset: Int, + musicFolderId: String? + ): List { + throw OfflineException("getAlbumList2 isn't available in offline mode") + } + @Throws(Exception::class) override fun updateJukeboxPlaylist(ids: List?): JukeboxStatus { throw OfflineException("Jukebox not available in offline mode") @@ -389,16 +399,6 @@ class OfflineMusicService : MusicService, KoinComponent { throw OfflineException("Music folders not available in offline mode") } - @Throws(OfflineException::class) - override fun getAlbumList2( - type: String, - size: Int, - offset: Int, - musicFolderId: String? - ): MusicDirectory { - throw OfflineException("getAlbumList2 isn't available in offline mode") - } - @Throws(OfflineException::class) override fun getVideoUrl(id: String): String? { throw OfflineException("getVideoUrl isn't available in offline mode") @@ -512,7 +512,6 @@ class OfflineMusicService : MusicService, KoinComponent { return album } - /* * Extracts some basic data from a File object and applies it to an Album or Entry */ @@ -531,7 +530,6 @@ class OfflineMusicService : MusicService, KoinComponent { } } - /* * More extensive variant of Child.populateWithDataFrom(), which also parses the ID3 tags of * a given track file. @@ -559,7 +557,7 @@ class OfflineMusicService : MusicService, KoinComponent { artist = meta.artist ?: file.parentFile!!.parentFile!!.name album = meta.album ?: file.parentFile!!.name - title = meta.title?: title + title = meta.title ?: title isVideo = meta.hasVideo != null track = parseSlashedNumber(meta.track) discNumber = parseSlashedNumber(meta.disc) @@ -660,7 +658,6 @@ class OfflineMusicService : MusicService, KoinComponent { return closeness } - private fun listFilesRecursively(parent: File, children: MutableList) { for (file in FileUtil.listMediaFiles(parent)) { if (file.isFile) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 3f89c136..545fab3d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -350,7 +350,7 @@ open class RESTMusicService( size: Int, offset: Int, musicFolderId: String? - ): MusicDirectory { + ): List { val response = API.getAlbumList( fromName(type), size, @@ -361,11 +361,8 @@ open class RESTMusicService( musicFolderId ).execute().throwOnFailure() - val childList = response.body()!!.albumList.toDomainEntityList() - val result = MusicDirectory() - result.addAll(childList) - return result + return response.body()!!.albumList.toDomainEntityList() } @Throws(Exception::class) @@ -374,7 +371,7 @@ open class RESTMusicService( size: Int, offset: Int, musicFolderId: String? - ): MusicDirectory { + ): List { val response = API.getAlbumList2( fromName(type), size, @@ -385,10 +382,7 @@ open class RESTMusicService( musicFolderId ).execute().throwOnFailure() - val result = MusicDirectory() - result.addAll(response.body()!!.albumList.toDomainEntityList()) - - return result + return response.body()!!.albumList.toDomainEntityList() } @Throws(Exception::class) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.kt new file mode 100644 index 00000000..ec3ef5cd --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.kt @@ -0,0 +1,50 @@ +package org.moire.ultrasonic.util + +import java.util.Comparator +import org.moire.ultrasonic.domain.MusicDirectory + +class EntryByDiscAndTrackComparator : Comparator { + override fun compare(x: MusicDirectory.Child, y: MusicDirectory.Child): Int { + val discX = x.discNumber + val discY = y.discNumber + val trackX = if (x is MusicDirectory.Entry) x.track else null + val trackY = if (y is MusicDirectory.Entry) y.track else null + val albumX = x.album + val albumY = y.album + val pathX = x.path + val pathY = y.path + val albumComparison = compare(albumX, albumY) + if (albumComparison != 0) { + return albumComparison + } + val discComparison = compare(discX ?: 0, discY ?: 0) + if (discComparison != 0) { + return discComparison + } + val trackComparison = compare(trackX ?: 0, trackY ?: 0) + return if (trackComparison != 0) { + trackComparison + } else compare( + pathX ?: "", + pathY ?: "" + ) + } + + companion object { + private fun compare(a: Int, b: Int): Int { + return a.compareTo(b) + } + + private fun compare(a: String?, b: String?): Int { + if (a == null && b == null) { + return 0 + } + if (a == null) { + return -1 + } + return if (b == null) { + 1 + } else a.compareTo(b) + } + } +} diff --git a/ultrasonic/src/main/res/layout/list_item_more_button.xml b/ultrasonic/src/main/res/layout/list_item_more_button.xml new file mode 100644 index 00000000..8d9b886c --- /dev/null +++ b/ultrasonic/src/main/res/layout/list_item_more_button.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/search_buttons.xml b/ultrasonic/src/main/res/layout/search_buttons.xml deleted file mode 100644 index 1666bdd1..00000000 --- a/ultrasonic/src/main/res/layout/search_buttons.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - From f1e789ea9b95c67b9fa97f155ae602f8b2ba2a31 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 30 Nov 2021 20:53:10 +0100 Subject: [PATCH 21/33] Fixed search, put compareTo method into Interface --- .../org/moire/ultrasonic/domain/Artist.kt | 19 +-- .../moire/ultrasonic/domain/ArtistOrIndex.kt | 19 ++- .../moire/ultrasonic/domain/Identifiable.kt | 14 +-- .../moire/ultrasonic/domain/MusicDirectory.kt | 2 +- .../moire/ultrasonic/domain/SearchResult.kt | 2 +- .../ultrasonic/api/subsonic/models/Album.kt | 4 +- gradle.properties | 3 +- .../moire/ultrasonic/view/ArtistAdapter.java | 117 ------------------ .../moire/ultrasonic/adapters/AlbumHeader.kt | 4 - .../moire/ultrasonic/adapters/BaseAdapter.kt | 2 - .../ultrasonic/adapters/DividerBinder.kt | 4 - .../adapters/FolderSelectorBinder.kt | 4 - .../ultrasonic/adapters/MoreButtonBinder.kt | 8 +- .../ultrasonic/domain/APIAlbumConverter.kt | 2 +- .../ultrasonic/fragment/AlbumListFragment.kt | 8 +- .../ultrasonic/fragment/ArtistListFragment.kt | 48 +++---- .../ultrasonic/fragment/EntryListFragment.kt | 5 +- .../ultrasonic/fragment/SearchFragment.kt | 45 +++++-- .../fragment/TrackCollectionFragment.kt | 25 ++-- .../moire/ultrasonic/service/DownloadFile.kt | 4 - .../ultrasonic/service/OfflineMusicService.kt | 5 +- .../ultrasonic/service/RESTMusicService.kt | 1 - .../main/res/navigation/navigation_graph.xml | 5 +- 23 files changed, 117 insertions(+), 233 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/view/ArtistAdapter.java diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt index 1c8e6440..3da622c6 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt @@ -11,21 +11,4 @@ data class Artist( override var coverArt: String? = null, override var albumCount: Long? = null, override var closeness: Int = 0 -) : ArtistOrIndex(id) { - - fun compareTo(other: Artist): Int { - when { - this.closeness == other.closeness -> { - return 0 - } - this.closeness > other.closeness -> { - return -1 - } - else -> { - return 1 - } - } - } - - override fun compareTo(other: Identifiable) = compareTo(other as Artist) -} +) : ArtistOrIndex(id) diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt index 586f1dae..602cae66 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt @@ -15,4 +15,21 @@ abstract class ArtistOrIndex( open var albumCount: Long? = null, @Ignore open var closeness: Int = 0 -) : GenericEntry() +) : GenericEntry() { + + fun compareTo(other: ArtistOrIndex): Int { + when { + this.closeness == other.closeness -> { + return 0 + } + this.closeness > other.closeness -> { + return -1 + } + else -> { + return 1 + } + } + } + + override fun compareTo(other: Identifiable) = compareTo(other as ArtistOrIndex) +} diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Identifiable.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Identifiable.kt index b316ce8e..71a57502 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Identifiable.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Identifiable.kt @@ -3,19 +3,17 @@ package org.moire.ultrasonic.domain import androidx.room.Ignore abstract class GenericEntry : Identifiable { - abstract override val id: String @Ignore open val name: String? = null - 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 { val id: String + val longId: Long + get() = id.hashCode().toLong() + + override fun compareTo(other: Identifiable): Int { + return longId.compareTo(other.longId) + } } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt index b409acab..e316dc42 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt @@ -32,7 +32,7 @@ class MusicDirectory : ArrayList() { } } - abstract class Child : Identifiable, GenericEntry() { + abstract class Child : GenericEntry() { abstract override var id: String abstract var parent: String? abstract var isDirectory: Boolean diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt index 7c8ee9bd..ec576d8f 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt @@ -7,7 +7,7 @@ import org.moire.ultrasonic.domain.MusicDirectory.Entry * The result of a search. Contains matching artists, albums and songs. */ data class SearchResult( - val artists: List = listOf(), + val artists: List = listOf(), val albums: List = listOf(), val songs: List = listOf() ) diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Album.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Album.kt index 801634c3..47ad7cd8 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Album.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Album.kt @@ -7,8 +7,8 @@ data class Album( val id: String = "", val parent: String = "", val album: String = "", - val title: String = "", - val name: String = "", + val title: String? = null, + val name: String? = null, val discNumber: Int = 0, val coverArt: String = "", val songCount: Int = 0, diff --git a/gradle.properties b/gradle.properties index 19077ae1..2635181c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,8 @@ org.gradle.parallel=true org.gradle.daemon=true org.gradle.configureondemand=true org.gradle.caching=true -org.gradle.jvmargs=-Xmx2g +org.gradle.jvmargs=-Xmx2g -XX:+UseParallelGC + kotlin.incremental=true kotlin.caching.enabled=true diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ArtistAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ArtistAdapter.java deleted file mode 100644 index b29de4e6..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ArtistAdapter.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2010 (C) Sindre Mehus - */ -package org.moire.ultrasonic.view; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.SectionIndexer; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.Artist; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.List; - -/** - * @author Sindre Mehus - */ -public class ArtistAdapter extends ArrayAdapter implements SectionIndexer -{ - private final LayoutInflater layoutInflater; - - // Both arrays are indexed by section ID. - private final Object[] sections; - private final Integer[] positions; - - public ArtistAdapter(Context context, List artists) - { - super(context, R.layout.list_item_generic, artists); - - layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - Collection sectionSet = new LinkedHashSet(30); - List positionList = new ArrayList(30); - - for (int i = 0; i < artists.size(); i++) - { - Artist artist = artists.get(i); - String index = artist.getIndex(); - - if (!sectionSet.contains(index)) - { - sectionSet.add(index); - positionList.add(i); - } - } - - sections = sectionSet.toArray(new Object[0]); - positions = positionList.toArray(new Integer[0]); - } - - @NonNull - @Override - public View getView( - int position, - @Nullable View convertView, - @NonNull ViewGroup parent - ) { - View rowView = convertView; - if (rowView == null) { - rowView = layoutInflater.inflate(R.layout.list_item_generic, parent, false); - } - ((TextView) rowView).setText(getItem(position).getName()); - - return rowView; - } - - @Override - public Object[] getSections() - { - return sections; - } - - @Override - public int getPositionForSection(int section) - { - return positions.length > section ? positions[section] : 0; - } - - @Override - public int getSectionForPosition(int pos) - { - for (int i = 0; i < sections.length - 1; i++) - { - if (pos < positions[i + 1]) - { - return i; - } - } - - return sections.length - 1; - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumHeader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumHeader.kt index 978ead6f..da300de0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumHeader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumHeader.kt @@ -88,8 +88,4 @@ class AlbumHeader( override val longId: Long get() = -1L - - override fun compareTo(other: Identifiable): Int { - return this.longId.compareTo(other.longId) - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index d17f25e2..2deaf433 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -52,8 +52,6 @@ class BaseAdapter : MultiTypeAdapter() { return mDiffer.currentList[position] } - // override getIt - override var items: List get() = getCurrentList() set(value) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt index b4f4627c..6e862163 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt @@ -41,9 +41,5 @@ class DividerBinder : ItemViewBinder Unit) - ): Identifiable { + ) : Identifiable { override val id: String get() = stringId.toString() - override val longId: Long - get() = stringId.toLong() - - override fun compareTo(other: Identifiable): Int = longId.compareTo(other.longId) } - } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt index acccda31..eb42d409 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt @@ -7,7 +7,7 @@ import org.moire.ultrasonic.api.subsonic.models.Album fun Album.toDomainEntity(): MusicDirectory.Album = MusicDirectory.Album( id = this@toDomainEntity.id, - title = this@toDomainEntity.title, + title = this@toDomainEntity.name ?: this@toDomainEntity.title, album = this@toDomainEntity.album, coverArt = this@toDomainEntity.coverArt, artist = this@toDomainEntity.artist, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index e719e8f8..92e4ed2e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -9,15 +9,12 @@ package org.moire.ultrasonic.fragment import android.os.Bundle import android.view.View -import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.RecyclerView import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.AlbumRowBinder -import org.moire.ultrasonic.adapters.FolderSelectorBinder -import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.model.AlbumListModel import org.moire.ultrasonic.util.Constants @@ -40,7 +37,10 @@ class AlbumListFragment : EntryListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { + override fun getLiveData( + args: Bundle?, + refresh: Boolean + ): LiveData> { if (args == null) throw IllegalArgumentException("Required arguments are missing") val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) || refresh diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 73346de6..36490411 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -2,25 +2,20 @@ package org.moire.ultrasonic.fragment import android.os.Bundle import android.view.View -import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData +import androidx.navigation.NavController import androidx.navigation.fragment.findNavController import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.ArtistRowBinder -import org.moire.ultrasonic.adapters.FolderSelectorBinder import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.ArtistOrIndex -import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Index -import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.model.ArtistListModel import org.moire.ultrasonic.util.Constants /** - * Displays the list of Artists from the media library - * - * FIXME: FOLDER HEADER NOT POPULATED ON FIST LOAD + * Displays the list of Artists or Indexes (folders) from the media library */ class ArtistListFragment : EntryListFragment() { @@ -60,23 +55,32 @@ class ArtistListFragment : EntryListFragment() { * If we are showing artists, we need to go to AlbumList */ override fun onItemClick(item: ArtistOrIndex) { - val bundle = Bundle() + Companion.onItemClick(item, findNavController()) + } - // Common arguments - 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)) + companion object { + fun onItemClick(item: ArtistOrIndex, navController: NavController) { + val bundle = Bundle() - // Check type - if (item is Index) { - findNavController().navigate(R.id.artistsListToTrackCollection, bundle) - } else { - bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST) - bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) - findNavController().navigate(R.id.artistsListToAlbumsList, bundle) + // Common arguments + 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)) + + // Check type + if (item is Index) { + navController.navigate(R.id.artistsListToTrackCollection, bundle) + } else { + bundle.putString( + Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, + Constants.ALBUMS_OF_ARTIST + ) + bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) + bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) + bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) + navController.navigate(R.id.artistsListToAlbumsList, bundle) + } } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 00726af2..ffb127f0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -9,7 +9,6 @@ import androidx.navigation.fragment.findNavController import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.FolderSelectorBinder import org.moire.ultrasonic.domain.Artist -import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.domain.GenericEntry import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.service.RxBus @@ -50,6 +49,10 @@ abstract class EntryListFragment : MultiListFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // Call a cheap function on ServerSettingsModel to make sure it is initialized by Koin, + // because it can't be initialized from inside the callback + serverSettingsModel.toString() + RxBus.musicFolderChangedEventObservable.subscribe { if (!listModel.isOffline()) { val currentSetting = listModel.activeServer diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index ded120e4..eeddb97b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -3,17 +3,16 @@ package org.moire.ultrasonic.fragment import android.app.SearchManager import android.content.Context import android.os.Bundle -import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import android.widget.ListAdapter import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.viewModelScope import androidx.navigation.Navigation +import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -26,7 +25,9 @@ import org.moire.ultrasonic.adapters.MoreButtonBinder import org.moire.ultrasonic.adapters.MoreButtonBinder.MoreButton import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.domain.Identifiable +import org.moire.ultrasonic.domain.Index import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle @@ -41,12 +42,10 @@ import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util.toast -import org.moire.ultrasonic.view.ArtistAdapter import timber.log.Timber /** * Initiates a search on the media library and displays the results - * FIXME: Artist click, display */ class SearchFragment : MultiListFragment(), KoinComponent { private var searchResult: SearchResult? = null @@ -265,11 +264,28 @@ class SearchFragment : MultiListFragment(), KoinComponent { populateList(listModel.trimResultLength(searchResult!!, maxSongs = Int.MAX_VALUE)) } - private fun onArtistSelected(artist: Artist) { + private fun onArtistSelected(item: ArtistOrIndex) { val bundle = Bundle() - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, artist.id) - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.id) - Navigation.findNavController(requireView()).navigate(R.id.searchToSelectAlbum, bundle) + + // Common arguments + 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)) + + // Check type + if (item is Index) { + findNavController().navigate(R.id.searchToTrackCollection, bundle) + } else { + bundle.putString( + Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, + Constants.ALBUMS_OF_ARTIST + ) + bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) + bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) + bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) + findNavController().navigate(R.id.searchToAlbumsList, bundle) + } } private fun onAlbumSelected(album: MusicDirectory.Album, autoplay: Boolean) { @@ -278,14 +294,21 @@ class SearchFragment : MultiListFragment(), KoinComponent { bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, album.title) bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, album.isDirectory) bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoplay) - Navigation.findNavController(requireView()).navigate(R.id.searchToSelectAlbum, bundle) + Navigation.findNavController(requireView()).navigate(R.id.searchToTrackCollection, bundle) } private fun onSongSelected(song: MusicDirectory.Entry, append: Boolean) { if (!append) { mediaPlayerController.clear() } - mediaPlayerController.addToPlaylist(listOf(song), false, false, false, false, false) + mediaPlayerController.addToPlaylist( + listOf(song), + save = false, + autoPlay = false, + playNext = false, + shuffle = false, + newPlaylist = false + ) mediaPlayerController.play(mediaPlayerController.playlistSize - 1) toast(context, resources.getQuantityString(R.plurals.select_album_n_songs_added, 1, 1)) } @@ -304,7 +327,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { override fun onItemClick(item: Identifiable) { when (item) { - is Artist -> { + is ArtistOrIndex -> { onArtistSelected(item) } is MusicDirectory.Entry -> { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 5cd254a5..ecd62571 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -49,13 +49,11 @@ import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util /** - * * Displays a group of tracks, eg. the songs of an album, of a playlist etc. * * In most cases the data should be just a list of Entries, but there are some cases * where the list can contain Albums as well. This happens especially when having ID3 tags disabled, * or using Offline mode, both in which Indexes instead of Artists are being used. - * */ @Suppress("TooManyFunctions") open class TrackCollectionFragment : MultiListFragment() { @@ -96,7 +94,7 @@ open class TrackCollectionFragment : MultiListFragment() { // Setup refresh handler refreshListView = view.findViewById(refreshListId) refreshListView?.setOnRefreshListener { - refreshData(true) + getLiveData(arguments, true) } // TODO: remove special casing for songsForGenre @@ -209,12 +207,6 @@ open class TrackCollectionFragment : MultiListFragment() { refreshListView?.isRefreshing = false } - private fun refreshData(refresh: Boolean = false) { - val args = getArgumentsClone() - args.putBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, refresh) - getLiveData(args) - } - override fun onPrepareOptionsMenu(menu: Menu) { super.onPrepareOptionsMenu(menu) playAllButton = menu.findItem(R.id.select_album_play_all) @@ -293,8 +285,6 @@ open class TrackCollectionFragment : MultiListFragment() { } val isArtist = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false) ?: false - - // FIXME WHICH id if no arguments? val id = arguments?.getString(Constants.INTENT_EXTRA_NAME_ID) if (hasSubFolders && id != null) { @@ -565,7 +555,10 @@ open class TrackCollectionFragment : MultiListFragment() { } @Suppress("LongMethod") - override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { + override fun getLiveData( + args: Bundle?, + refresh: Boolean + ): LiveData> { 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) @@ -588,7 +581,7 @@ open class TrackCollectionFragment : MultiListFragment() { val albumListOffset = args.getInt( Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0 ) - val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true) || refresh + val refresh2 = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true) || refresh listModel.viewModelScope.launch(handler) { refreshListView?.isRefreshing = true @@ -610,7 +603,7 @@ open class TrackCollectionFragment : MultiListFragment() { listModel.getStarred() } else if (getVideos != 0) { setTitle(R.string.main_videos) - listModel.getVideos(refresh) + listModel.getVideos(refresh2) } else if (getRandomTracks != 0) { setTitle(R.string.main_songs_random) listModel.getRandom(albumListSize) @@ -618,12 +611,12 @@ open class TrackCollectionFragment : MultiListFragment() { setTitle(name) if (!isOffline() && Settings.shouldUseId3Tags) { if (isAlbum) { - listModel.getAlbum(refresh, id!!, name) + listModel.getAlbum(refresh2, id!!, name) } else { throw IllegalAccessException("Use AlbumFragment instead!") } } else { - listModel.getMusicDirectory(refresh, id!!, name) + listModel.getMusicDirectory(refresh2, id!!, name) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index 90c8bf45..74244c78 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -436,10 +436,6 @@ 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 } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index fad431f7..a7549a47 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -24,6 +24,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.Genre @@ -122,7 +123,7 @@ class OfflineMusicService : MusicService, KoinComponent { } override fun search(criteria: SearchCriteria): SearchResult { - val artists: MutableList = ArrayList() + val artists: MutableList = ArrayList() val albums: MutableList = ArrayList() val songs: MutableList = ArrayList() val root = FileUtil.musicDirectory @@ -131,7 +132,7 @@ class OfflineMusicService : MusicService, KoinComponent { val artistName = artistFile.name if (artistFile.isDirectory) { if (matchCriteria(criteria, artistName).also { closeness = it } > 0) { - val artist = Artist(artistFile.path) + val artist = Index(artistFile.path) artist.index = artistFile.name.substring(0, 1) artist.name = artistName artist.closeness = closeness diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 545fab3d..74c7c3ea 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -361,7 +361,6 @@ open class RESTMusicService( musicFolderId ).execute().throwOnFailure() - return response.body()!!.albumList.toDomainEntityList() } diff --git a/ultrasonic/src/main/res/navigation/navigation_graph.xml b/ultrasonic/src/main/res/navigation/navigation_graph.xml index 0ab8f38c..90f978a6 100644 --- a/ultrasonic/src/main/res/navigation/navigation_graph.xml +++ b/ultrasonic/src/main/res/navigation/navigation_graph.xml @@ -60,8 +60,11 @@ android:id="@+id/searchFragment" android:name="org.moire.ultrasonic.fragment.SearchFragment" > + Date: Tue, 30 Nov 2021 21:21:50 +0100 Subject: [PATCH 22/33] Shorten INTENT Constants names --- .../ultrasonic/fragment/LyricsFragment.java | 4 +- .../fragment/PlaylistsFragment.java | 20 ++--- .../ultrasonic/fragment/PodcastFragment.java | 2 +- .../fragment/SelectGenreFragment.java | 6 +- .../ultrasonic/fragment/SharesFragment.java | 4 +- .../provider/UltrasonicAppWidgetProvider.java | 2 +- .../ultrasonic/activity/NavigationActivity.kt | 6 +- .../ultrasonic/fragment/AlbumListFragment.kt | 14 ++-- .../ultrasonic/fragment/ArtistListFragment.kt | 21 ++--- .../ultrasonic/fragment/EntryListFragment.kt | 8 +- .../moire/ultrasonic/fragment/MainFragment.kt | 18 ++-- .../ultrasonic/fragment/MultiListFragment.kt | 2 +- .../ultrasonic/fragment/NowPlayingFragment.kt | 12 +-- .../ultrasonic/fragment/PlayerFragment.kt | 22 ++--- .../ultrasonic/fragment/SearchFragment.kt | 35 ++++---- .../fragment/TrackCollectionFragment.kt | 82 +++++++++---------- .../moire/ultrasonic/model/AlbumListModel.kt | 16 ++-- .../ultrasonic/service/CachedMusicService.kt | 4 +- .../ultrasonic/service/MediaPlayerService.kt | 2 +- .../ultrasonic/subsonic/DownloadHandler.kt | 2 +- .../moire/ultrasonic/subsonic/ShareHandler.kt | 2 +- .../org/moire/ultrasonic/util/Constants.kt | 50 +++++------ 22 files changed, 161 insertions(+), 173 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/LyricsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/LyricsFragment.java index e461db64..f7f87b17 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/LyricsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/LyricsFragment.java @@ -76,8 +76,8 @@ public class LyricsFragment extends Fragment { { Bundle arguments = getArguments(); if (arguments == null) return null; - String artist = arguments.getString(Constants.INTENT_EXTRA_NAME_ARTIST); - String title = arguments.getString(Constants.INTENT_EXTRA_NAME_TITLE); + String artist = arguments.getString(Constants.INTENT_ARTIST); + String title = arguments.getString(Constants.INTENT_TITLE); MusicService musicService = MusicServiceFactory.getMusicService(); return musicService.getLyrics(artist, title); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java index e78777b6..5d11c45e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java @@ -102,9 +102,9 @@ public class PlaylistsFragment extends Fragment { } Bundle bundle = new Bundle(); - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, playlist.getId()); - bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId()); - bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName()); + bundle.putString(Constants.INTENT_ID, playlist.getId()); + bundle.putString(Constants.INTENT_PLAYLIST_ID, playlist.getId()); + bundle.putString(Constants.INTENT_PLAYLIST_NAME, playlist.getName()); Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle); } }); @@ -187,16 +187,16 @@ public class PlaylistsFragment extends Fragment { downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), false, false, false, false, true, false, false); } else if (itemId == R.id.playlist_menu_play_now) { bundle = new Bundle(); - bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId()); - bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName()); - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true); + bundle.putString(Constants.INTENT_PLAYLIST_ID, playlist.getId()); + bundle.putString(Constants.INTENT_PLAYLIST_NAME, playlist.getName()); + bundle.putBoolean(Constants.INTENT_AUTOPLAY, true); Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle); } else if (itemId == R.id.playlist_menu_play_shuffled) { bundle = new Bundle(); - bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId()); - bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName()); - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true); - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, true); + bundle.putString(Constants.INTENT_PLAYLIST_ID, playlist.getId()); + bundle.putString(Constants.INTENT_PLAYLIST_NAME, playlist.getName()); + bundle.putBoolean(Constants.INTENT_AUTOPLAY, true); + bundle.putBoolean(Constants.INTENT_SHUFFLE, true); Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle); } else if (itemId == R.id.playlist_menu_delete) { deletePlaylist(playlist); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java index eb068046..00210dd0 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java @@ -75,7 +75,7 @@ public class PodcastFragment extends Fragment { } Bundle bundle = new Bundle(); - bundle.putString(Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID, pc.getId()); + bundle.putString(Constants.INTENT_PODCAST_CHANNEL_ID, pc.getId()); Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle); } }); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java index 47290c02..dc6f3382 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java @@ -75,9 +75,9 @@ public class SelectGenreFragment extends Fragment { if (genre != null) { Bundle bundle = new Bundle(); - bundle.putString(Constants.INTENT_EXTRA_NAME_GENRE_NAME, genre.getName()); - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Settings.getMaxSongs()); - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0); + bundle.putString(Constants.INTENT_GENRE_NAME, genre.getName()); + bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, Settings.getMaxSongs()); + bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0); Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java index 4da42549..958774bf 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java @@ -104,8 +104,8 @@ public class SharesFragment extends Fragment { } Bundle bundle = new Bundle(); - bundle.putString(Constants.INTENT_EXTRA_NAME_SHARE_ID, share.getId()); - bundle.putString(Constants.INTENT_EXTRA_NAME_SHARE_NAME, share.getName()); + bundle.putString(Constants.INTENT_SHARE_ID, share.getId()); + bundle.putString(Constants.INTENT_SHARE_NAME, share.getName()); Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle); } }); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java index 619484e2..cd88a66e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java @@ -191,7 +191,7 @@ public class UltrasonicAppWidgetProvider extends AppWidgetProvider { Intent intent = new Intent(context, NavigationActivity.class).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); if (playerActive) - intent.putExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, true); + intent.putExtra(Constants.INTENT_SHOW_PLAYER, true); intent.setAction("android.intent.action.MAIN"); intent.addCategory("android.intent.category.LAUNCHER"); diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 7ec5c6ff..65b1370e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -319,7 +319,7 @@ class NavigationActivity : AppCompatActivity() { super.onNewIntent(intent) if (intent == null) return - if (intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, false)) { + if (intent.getBooleanExtra(Constants.INTENT_SHOW_PLAYER, false)) { findNavController(R.id.nav_host_fragment).navigate(R.id.playerFragment) return } @@ -335,8 +335,8 @@ class NavigationActivity : AppCompatActivity() { suggestions.saveRecentQuery(query, null) val bundle = Bundle() - bundle.putString(Constants.INTENT_EXTRA_NAME_QUERY, query) - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoPlay) + bundle.putString(Constants.INTENT_QUERY, query) + bundle.putBoolean(Constants.INTENT_AUTOPLAY, autoPlay) findNavController(R.id.nav_host_fragment).navigate(R.id.searchFragment, bundle) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 92e4ed2e..f11ab127 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -43,8 +43,8 @@ class AlbumListFragment : EntryListFragment() { ): LiveData> { if (args == null) throw IllegalArgumentException("Required arguments are missing") - val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) || refresh - val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND) + val refresh = args.getBoolean(Constants.INTENT_REFRESH) || refresh + val append = args.getBoolean(Constants.INTENT_APPEND) return listModel.getAlbumList(refresh or append, refreshListView!!, args) } @@ -59,7 +59,7 @@ class AlbumListFragment : EntryListFragment() { // Triggered only when new data needs to be appended to the list // Add whatever code is needed to append new items to the bottom of the list val appendArgs = getArgumentsClone() - appendArgs.putBoolean(Constants.INTENT_EXTRA_NAME_APPEND, true) + appendArgs.putBoolean(Constants.INTENT_APPEND, true) getLiveData(appendArgs) } } @@ -80,10 +80,10 @@ class AlbumListFragment : EntryListFragment() { override fun onItemClick(item: MusicDirectory.Album) { val bundle = Bundle() - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id) - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, item.isDirectory) - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.title) - bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent) + bundle.putString(Constants.INTENT_ID, item.id) + bundle.putBoolean(Constants.INTENT_IS_ALBUM, item.isDirectory) + bundle.putString(Constants.INTENT_NAME, item.title) + bundle.putString(Constants.INTENT_PARENT_ID, item.parent) findNavController().navigate(R.id.trackCollectionFragment, bundle) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 36490411..10090b65 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -33,7 +33,7 @@ class ArtistListFragment : EntryListFragment() { * The central function to pass a query to the model and return a LiveData object */ override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { - val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false || refresh + val refresh = args?.getBoolean(Constants.INTENT_REFRESH) ?: false || refresh return listModel.getItems(refresh, refreshListView!!) } @@ -63,22 +63,19 @@ class ArtistListFragment : EntryListFragment() { val bundle = Bundle() // Common arguments - 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)) + bundle.putString(Constants.INTENT_ID, item.id) + bundle.putString(Constants.INTENT_NAME, item.name) + bundle.putString(Constants.INTENT_PARENT_ID, item.id) + bundle.putBoolean(Constants.INTENT_ARTIST, (item is Artist)) // Check type if (item is Index) { navController.navigate(R.id.artistsListToTrackCollection, bundle) } else { - bundle.putString( - Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, - Constants.ALBUMS_OF_ARTIST - ) - bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) + bundle.putString(Constants.INTENT_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST) + bundle.putString(Constants.INTENT_ALBUM_LIST_TITLE, item.name) + bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, 1000) + bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0) navController.navigate(R.id.artistsListToAlbumsList, bundle) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index ffb127f0..22942703 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -39,10 +39,10 @@ abstract class EntryListFragment : MultiListFragment() { 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)) + bundle.putString(Constants.INTENT_ID, item.id) + bundle.putString(Constants.INTENT_NAME, item.name) + bundle.putString(Constants.INTENT_PARENT_ID, item.id) + bundle.putBoolean(Constants.INTENT_ARTIST, (item is Artist)) findNavController().navigate(R.id.trackCollectionFragment, bundle) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt index 56d33651..940fb1b5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt @@ -201,21 +201,21 @@ class MainFragment : Fragment(), KoinComponent { private fun showStarredSongs() { val bundle = Bundle() - bundle.putInt(Constants.INTENT_EXTRA_NAME_STARRED, 1) + bundle.putInt(Constants.INTENT_STARRED, 1) Navigation.findNavController(requireView()).navigate(R.id.mainToTrackCollection, bundle) } private fun showRandomSongs() { val bundle = Bundle() - bundle.putInt(Constants.INTENT_EXTRA_NAME_RANDOM, 1) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Settings.maxSongs) + bundle.putInt(Constants.INTENT_RANDOM, 1) + bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, Settings.maxSongs) Navigation.findNavController(requireView()).navigate(R.id.mainToTrackCollection, bundle) } private fun showArtists() { val bundle = Bundle() bundle.putString( - Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, + Constants.INTENT_ALBUM_LIST_TITLE, requireContext().resources.getString(R.string.main_artists_title) ) Navigation.findNavController(requireView()).navigate(R.id.mainToArtistList, bundle) @@ -224,10 +224,10 @@ class MainFragment : Fragment(), KoinComponent { private fun showAlbumList(type: String, titleIndex: Int) { val bundle = Bundle() val title = requireContext().resources.getString(titleIndex, "") - bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type) - bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, title) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Settings.maxAlbums) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) + bundle.putString(Constants.INTENT_ALBUM_LIST_TYPE, type) + bundle.putString(Constants.INTENT_ALBUM_LIST_TITLE, title) + bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, Settings.maxAlbums) + bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0) Navigation.findNavController(requireView()).navigate(R.id.mainToAlbumList, bundle) } @@ -237,7 +237,7 @@ class MainFragment : Fragment(), KoinComponent { private fun showVideos() { val bundle = Bundle() - bundle.putInt(Constants.INTENT_EXTRA_NAME_VIDEOS, 1) + bundle.putInt(Constants.INTENT_VIDEOS, 1) Navigation.findNavController(requireView()).navigate(R.id.mainToTrackCollection, bundle) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index d0230a65..0c819730 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -114,7 +114,7 @@ abstract class MultiListFragment : Fragment() { super.onViewCreated(view, savedInstanceState) // Set the title if available - setTitle(arguments?.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE)) + setTitle(arguments?.getString(Constants.INTENT_ALBUM_LIST_TITLE)) // Setup refresh handler refreshListView = view.findViewById(refreshListId) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index 1171738f..4d700410 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -123,15 +123,15 @@ class NowPlayingFragment : Fragment() { val bundle = Bundle() if (Settings.shouldUseId3Tags) { - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, true) - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, song.albumId) + bundle.putBoolean(Constants.INTENT_IS_ALBUM, true) + bundle.putString(Constants.INTENT_ID, song.albumId) } else { - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, false) - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, song.parent) + bundle.putBoolean(Constants.INTENT_IS_ALBUM, false) + bundle.putString(Constants.INTENT_ID, song.parent) } - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, song.album) - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, song.album) + bundle.putString(Constants.INTENT_NAME, song.album) + bundle.putString(Constants.INTENT_NAME, song.album) Navigation.findNavController(requireActivity(), R.id.nav_host_fragment) .navigate(R.id.trackCollectionFragment, bundle) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 75c24988..37451286 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -339,7 +339,7 @@ class PlayerFragment : registerForContextMenu(playlistView) if (arguments != null && requireArguments().getBoolean( - Constants.INTENT_EXTRA_NAME_SHUFFLE, + Constants.INTENT_SHUFFLE, false ) ) { @@ -579,10 +579,10 @@ class PlayerFragment : if (Settings.shouldUseId3Tags) { bundle = Bundle() - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, entry.artistId) - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.artist) - bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.artistId) - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true) + bundle.putString(Constants.INTENT_ID, entry.artistId) + bundle.putString(Constants.INTENT_NAME, entry.artist) + bundle.putString(Constants.INTENT_PARENT_ID, entry.artistId) + bundle.putBoolean(Constants.INTENT_ARTIST, true) Navigation.findNavController(requireView()) .navigate(R.id.playerToSelectAlbum, bundle) } @@ -593,10 +593,10 @@ class PlayerFragment : val albumId = if (Settings.shouldUseId3Tags) entry.albumId else entry.parent bundle = Bundle() - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, albumId) - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.album) - bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.parent) - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, true) + bundle.putString(Constants.INTENT_ID, albumId) + bundle.putString(Constants.INTENT_NAME, entry.album) + bundle.putString(Constants.INTENT_PARENT_ID, entry.parent) + bundle.putBoolean(Constants.INTENT_IS_ALBUM, true) Navigation.findNavController(requireView()) .navigate(R.id.playerToSelectAlbum, bundle) return true @@ -605,8 +605,8 @@ class PlayerFragment : if (entry == null) return false bundle = Bundle() - bundle.putString(Constants.INTENT_EXTRA_NAME_ARTIST, entry.artist) - bundle.putString(Constants.INTENT_EXTRA_NAME_TITLE, entry.title) + bundle.putString(Constants.INTENT_ARTIST, entry.artist) + bundle.putString(Constants.INTENT_TITLE, entry.title) Navigation.findNavController(requireView()).navigate(R.id.playerToLyrics, bundle) return true } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index eeddb97b..8acf907d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -127,8 +127,8 @@ class SearchFragment : MultiListFragment(), KoinComponent { // Fragment was started with a query (e.g. from voice search), try to execute search right away val arguments = arguments if (arguments != null) { - val query = arguments.getString(Constants.INTENT_EXTRA_NAME_QUERY) - val autoPlay = arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) + val query = arguments.getString(Constants.INTENT_QUERY) + val autoPlay = arguments.getBoolean(Constants.INTENT_AUTOPLAY, false) if (query != null) { return search(query, autoPlay) } @@ -149,8 +149,8 @@ class SearchFragment : MultiListFragment(), KoinComponent { val arguments = arguments val autoPlay = arguments != null && - arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) - val query = arguments?.getString(Constants.INTENT_EXTRA_NAME_QUERY) + arguments.getBoolean(Constants.INTENT_AUTOPLAY, false) + val query = arguments?.getString(Constants.INTENT_QUERY) // If started with a query, enter it to the searchView if (query != null) { @@ -268,32 +268,29 @@ class SearchFragment : MultiListFragment(), KoinComponent { val bundle = Bundle() // Common arguments - 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)) + bundle.putString(Constants.INTENT_ID, item.id) + bundle.putString(Constants.INTENT_NAME, item.name) + bundle.putString(Constants.INTENT_PARENT_ID, item.id) + bundle.putBoolean(Constants.INTENT_ARTIST, (item is Artist)) // Check type if (item is Index) { findNavController().navigate(R.id.searchToTrackCollection, bundle) } else { - bundle.putString( - Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, - Constants.ALBUMS_OF_ARTIST - ) - bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) + bundle.putString(Constants.INTENT_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST) + bundle.putString(Constants.INTENT_ALBUM_LIST_TITLE, item.name) + bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, 1000) + bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0) findNavController().navigate(R.id.searchToAlbumsList, bundle) } } private fun onAlbumSelected(album: MusicDirectory.Album, autoplay: Boolean) { val bundle = Bundle() - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, album.id) - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, album.title) - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, album.isDirectory) - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoplay) + bundle.putString(Constants.INTENT_ID, album.id) + bundle.putString(Constants.INTENT_NAME, album.title) + bundle.putBoolean(Constants.INTENT_IS_ALBUM, album.isDirectory) + bundle.putBoolean(Constants.INTENT_AUTOPLAY, autoplay) Navigation.findNavController(requireView()).navigate(R.id.searchToTrackCollection, bundle) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index ecd62571..8e4dc778 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -284,8 +284,8 @@ open class TrackCollectionFragment : MultiListFragment() { } } - val isArtist = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false) ?: false - val id = arguments?.getString(Constants.INTENT_EXTRA_NAME_ID) + val isArtist = arguments?.getBoolean(Constants.INTENT_ARTIST, false) ?: false + val id = arguments?.getString(Constants.INTENT_ID) if (hasSubFolders && id != null) { downloadHandler.downloadRecursively( @@ -428,7 +428,7 @@ open class TrackCollectionFragment : MultiListFragment() { // Hide more button when results are less than album list size if (musicDirectory.size < requireArguments().getInt( - Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0 + Constants.INTENT_ALBUM_LIST_SIZE, 0 ) ) { moreButton!!.visibility = View.GONE @@ -437,15 +437,15 @@ open class TrackCollectionFragment : MultiListFragment() { } moreButton!!.setOnClickListener { - val theGenre = requireArguments().getString(Constants.INTENT_EXTRA_NAME_GENRE_NAME) - val size = requireArguments().getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) + val theGenre = requireArguments().getString(Constants.INTENT_GENRE_NAME) + val size = requireArguments().getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) val theOffset = requireArguments().getInt( - Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0 + Constants.INTENT_ALBUM_LIST_OFFSET, 0 ) + size val bundle = Bundle() - bundle.putString(Constants.INTENT_EXTRA_NAME_GENRE_NAME, theGenre) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, size) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, theOffset) + bundle.putString(Constants.INTENT_GENRE_NAME, theGenre) + bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, size) + bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, theOffset) Navigation.findNavController(requireView()) .navigate(R.id.trackCollectionFragment, bundle) @@ -472,7 +472,7 @@ open class TrackCollectionFragment : MultiListFragment() { } } - val listSize = arguments?.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) ?: 0 + val listSize = arguments?.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) ?: 0 // Hide select button for video lists and singular selection lists selectButton!!.isVisible = !allVideos && viewAdapter.hasMultipleSelection() && songCount > 0 @@ -482,15 +482,15 @@ open class TrackCollectionFragment : MultiListFragment() { moreButton!!.visibility = View.GONE } else { moreButton!!.visibility = View.VISIBLE - if (arguments?.getInt(Constants.INTENT_EXTRA_NAME_RANDOM, 0) ?: 0 > 0) { + if (arguments?.getInt(Constants.INTENT_RANDOM, 0) ?: 0 > 0) { moreButton!!.setOnClickListener { val offset = requireArguments().getInt( - Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0 + Constants.INTENT_ALBUM_LIST_OFFSET, 0 ) + listSize val bundle = Bundle() - bundle.putInt(Constants.INTENT_EXTRA_NAME_RANDOM, 1) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, listSize) - bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, offset) + bundle.putInt(Constants.INTENT_RANDOM, 1) + bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, listSize) + bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, offset) Navigation.findNavController(requireView()).navigate( R.id.trackCollectionFragment, bundle ) @@ -505,7 +505,7 @@ open class TrackCollectionFragment : MultiListFragment() { enableButtons() val isAlbumList = arguments?.containsKey( - Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE + Constants.INTENT_ALBUM_LIST_TYPE ) ?: false playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos @@ -515,7 +515,7 @@ open class TrackCollectionFragment : MultiListFragment() { shareButton?.isVisible = shareButtonVisible if (songCount > 0 && listModel.showHeader) { - val intentAlbumName = arguments?.getString(Constants.INTENT_EXTRA_NAME_NAME, "") + val intentAlbumName = arguments?.getString(Constants.INTENT_NAME, "") val albumHeader = AlbumHeader(it, intentAlbumName) val mixedList: MutableList = mutableListOf(albumHeader) mixedList.addAll(entryList) @@ -524,11 +524,11 @@ open class TrackCollectionFragment : MultiListFragment() { viewAdapter.submitList(entryList) } - val playAll = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) ?: false + val playAll = arguments?.getBoolean(Constants.INTENT_AUTOPLAY, false) ?: false if (playAll && songCount > 0) { playAll( - arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false) ?: false, + arguments?.getBoolean(Constants.INTENT_SHUFFLE, false) ?: false, false ) } @@ -560,28 +560,22 @@ open class TrackCollectionFragment : MultiListFragment() { refresh: Boolean ): LiveData> { 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 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 id = args.getString(Constants.INTENT_ID) + val isAlbum = args.getBoolean(Constants.INTENT_IS_ALBUM, false) + val name = args.getString(Constants.INTENT_NAME) + val playlistId = args.getString(Constants.INTENT_PLAYLIST_ID) + val podcastChannelId = args.getString(Constants.INTENT_PODCAST_CHANNEL_ID) + val playlistName = args.getString(Constants.INTENT_PLAYLIST_NAME) + val shareId = args.getString(Constants.INTENT_SHARE_ID) + val shareName = args.getString(Constants.INTENT_SHARE_NAME) + val genreName = args.getString(Constants.INTENT_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 refresh2 = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true) || refresh + val getStarredTracks = args.getInt(Constants.INTENT_STARRED, 0) + val getVideos = args.getInt(Constants.INTENT_VIDEOS, 0) + val getRandomTracks = args.getInt(Constants.INTENT_RANDOM, 0) + val albumListSize = args.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) + val albumListOffset = args.getInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0) + val refresh2 = args.getBoolean(Constants.INTENT_REFRESH, true) || refresh listModel.viewModelScope.launch(handler) { refreshListView?.isRefreshing = true @@ -698,10 +692,10 @@ open class TrackCollectionFragment : MultiListFragment() { when { item.isDirectory -> { val bundle = Bundle() - bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id) - bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, item.isDirectory) - bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.title) - bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent) + bundle.putString(Constants.INTENT_ID, item.id) + bundle.putBoolean(Constants.INTENT_IS_ALBUM, item.isDirectory) + bundle.putString(Constants.INTENT_NAME, item.title) + bundle.putString(Constants.INTENT_PARENT_ID, item.parent) Navigation.findNavController(requireView()).navigate( R.id.trackCollectionFragment, bundle diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt index 0b935e36..816696c7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -24,7 +24,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) { ): LiveData> { // Don't reload the data if navigating back to the view that was active before. // This way, we keep the scroll position - val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!! + val albumListType = args.getString(Constants.INTENT_ALBUM_LIST_TYPE)!! if (refresh || list.value?.isEmpty() != false || albumListType != lastType) { lastType = albumListType @@ -51,10 +51,10 @@ class AlbumListModel(application: Application) : GenericListModel(application) { ) { super.load(isOffline, useId3Tags, musicService, refresh, args) - val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!! - val size = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) - var offset = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) - val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND, false) + val albumListType = args.getString(Constants.INTENT_ALBUM_LIST_TYPE)!! + val size = args.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) + var offset = args.getInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0) + val append = args.getBoolean(Constants.INTENT_APPEND, false) val musicDirectory: List val musicFolderId = if (showSelectFolderHeader(args)) { @@ -71,8 +71,8 @@ class AlbumListModel(application: Application) : GenericListModel(application) { return getAlbumsOfArtist( musicService, refresh, - args.getString(Constants.INTENT_EXTRA_NAME_ID, ""), - args.getString(Constants.INTENT_EXTRA_NAME_NAME, "") + args.getString(Constants.INTENT_ID, ""), + args.getString(Constants.INTENT_NAME, "") ) } @@ -106,7 +106,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) { override fun showSelectFolderHeader(args: Bundle?): Boolean { if (args == null) return false - val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!! + val albumListType = args.getString(Constants.INTENT_ALBUM_LIST_TYPE)!! val isAlphabetical = (albumListType == AlbumListType.SORTED_BY_NAME.toString()) || (albumListType == AlbumListType.SORTED_BY_ARTIST.toString()) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index 7ebbdd11..a0e79d73 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -416,7 +416,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, override fun getVideos(refresh: Boolean): MusicDirectory? { checkSettingsChanged() var cache = - if (refresh) null else cachedMusicDirectories[Constants.INTENT_EXTRA_NAME_VIDEOS] + if (refresh) null else cachedMusicDirectories[Constants.INTENT_VIDEOS] var dir = cache?.get() if (dir == null) { dir = musicService.getVideos(refresh) @@ -424,7 +424,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, Settings.directoryCacheTime.toLong(), TimeUnit.SECONDS ) cache.set(dir) - cachedMusicDirectories.put(Constants.INTENT_EXTRA_NAME_VIDEOS, cache) + cachedMusicDirectories.put(Constants.INTENT_VIDEOS, cache) } return dir } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index e503b36f..6c59af60 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -702,7 +702,7 @@ class MediaPlayerService : Service() { val intent = Intent(this, NavigationActivity::class.java) .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) val flags = PendingIntent.FLAG_UPDATE_CURRENT - intent.putExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, true) + intent.putExtra(Constants.INTENT_SHOW_PLAYER, true) return PendingIntent.getActivity(this, 0, intent, flags) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index 9bb0a61f..807ae3b1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -49,7 +49,7 @@ class DownloadHandler( false ) val playlistName: String? = fragment.arguments?.getString( - Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME + Constants.INTENT_PLAYLIST_NAME ) if (playlistName != null) { mediaPlayerController.suggestedPlaylistName = playlistName diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt index 13f9922f..227b2b94 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt @@ -80,7 +80,7 @@ class ShareHandler(val context: Context) { if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1) return null if (shareDetails.Entries.isEmpty()) { - fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_ID).ifNotNull { + fragment.arguments?.getString(Constants.INTENT_ID).ifNotNull { ids.add(it) } } else { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt index ea604676..3b4cf4f1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -16,31 +16,31 @@ object Constants { const val REST_CLIENT_ID = "Ultrasonic" // Names for intent extras. - const val INTENT_EXTRA_NAME_ID = "subsonic.id" - const val INTENT_EXTRA_NAME_NAME = "subsonic.name" - const val INTENT_EXTRA_NAME_ARTIST = "subsonic.artist" - const val INTENT_EXTRA_NAME_TITLE = "subsonic.title" - const val INTENT_EXTRA_NAME_AUTOPLAY = "subsonic.playall" - const val INTENT_EXTRA_NAME_QUERY = "subsonic.query" - const val INTENT_EXTRA_NAME_PLAYLIST_ID = "subsonic.playlist.id" - const val INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID = "subsonic.podcastChannel.id" - const val INTENT_EXTRA_NAME_PARENT_ID = "subsonic.parent.id" - const val INTENT_EXTRA_NAME_PLAYLIST_NAME = "subsonic.playlist.name" - const val INTENT_EXTRA_NAME_SHARE_ID = "subsonic.share.id" - const val INTENT_EXTRA_NAME_SHARE_NAME = "subsonic.share.name" - const val INTENT_EXTRA_NAME_ALBUM_LIST_TYPE = "subsonic.albumlisttype" - const val INTENT_EXTRA_NAME_ALBUM_LIST_TITLE = "subsonic.albumlisttitle" - const val INTENT_EXTRA_NAME_ALBUM_LIST_SIZE = "subsonic.albumlistsize" - const val INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset" - const val INTENT_EXTRA_NAME_SHUFFLE = "subsonic.shuffle" - const val INTENT_EXTRA_NAME_REFRESH = "subsonic.refresh" - const val INTENT_EXTRA_NAME_STARRED = "subsonic.starred" - const val INTENT_EXTRA_NAME_RANDOM = "subsonic.random" - const val INTENT_EXTRA_NAME_GENRE_NAME = "subsonic.genre" - const val INTENT_EXTRA_NAME_IS_ALBUM = "subsonic.isalbum" - const val INTENT_EXTRA_NAME_VIDEOS = "subsonic.videos" - const val INTENT_EXTRA_NAME_SHOW_PLAYER = "subsonic.showplayer" - const val INTENT_EXTRA_NAME_APPEND = "subsonic.append" + const val INTENT_ID = "subsonic.id" + const val INTENT_NAME = "subsonic.name" + const val INTENT_ARTIST = "subsonic.artist" + const val INTENT_TITLE = "subsonic.title" + const val INTENT_AUTOPLAY = "subsonic.playall" + const val INTENT_QUERY = "subsonic.query" + const val INTENT_PLAYLIST_ID = "subsonic.playlist.id" + const val INTENT_PODCAST_CHANNEL_ID = "subsonic.podcastChannel.id" + const val INTENT_PARENT_ID = "subsonic.parent.id" + const val INTENT_PLAYLIST_NAME = "subsonic.playlist.name" + const val INTENT_SHARE_ID = "subsonic.share.id" + const val INTENT_SHARE_NAME = "subsonic.share.name" + const val INTENT_ALBUM_LIST_TYPE = "subsonic.albumlisttype" + const val INTENT_ALBUM_LIST_TITLE = "subsonic.albumlisttitle" + const val INTENT_ALBUM_LIST_SIZE = "subsonic.albumlistsize" + const val INTENT_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset" + const val INTENT_SHUFFLE = "subsonic.shuffle" + const val INTENT_REFRESH = "subsonic.refresh" + const val INTENT_STARRED = "subsonic.starred" + const val INTENT_RANDOM = "subsonic.random" + const val INTENT_GENRE_NAME = "subsonic.genre" + const val INTENT_IS_ALBUM = "subsonic.isalbum" + const val INTENT_VIDEOS = "subsonic.videos" + const val INTENT_SHOW_PLAYER = "subsonic.showplayer" + const val INTENT_APPEND = "subsonic.append" // Names for Intent Actions const val CMD_PROCESS_KEYCODE = "org.moire.ultrasonic.CMD_PROCESS_KEYCODE" From f2948cd3dbde625c45c1ad382c188fea93c3f5b7 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 30 Nov 2021 21:50:53 +0100 Subject: [PATCH 23/33] Various fixes & cleanups --- .../SubsonicApiGetAlbumListRequestTest.kt | 6 +- .../api/subsonic/SubsonicApiSearchTwoTest.kt | 6 +- .../ultrasonic/api/subsonic/models/Album.kt | 1 + detekt-config.yml | 4 +- ultrasonic/lint-baseline.xml | 115 ++++++++------- .../moire/ultrasonic/adapters/BaseAdapter.kt | 1 - .../ultrasonic/adapters/DividerBinder.kt | 2 +- .../ultrasonic/adapters/TrackViewBinder.kt | 19 +-- .../ultrasonic/adapters/TrackViewHolder.kt | 26 +++- .../ultrasonic/fragment/AlbumListFragment.kt | 4 +- .../ultrasonic/fragment/ArtistListFragment.kt | 4 +- .../ultrasonic/fragment/BookmarksFragment.kt | 5 +- .../ultrasonic/fragment/PlayerFragment.kt | 16 ++- .../moire/ultrasonic/service/Downloader.kt | 4 +- .../service/MediaPlayerController.kt | 1 + .../res/layout/list_item_track_details.xml | 136 ++++++++++-------- .../src/main/res/layout/video_details.xml | 38 ----- .../src/main/res/layout/video_list_item.xml | 29 ---- .../domain/APISearchConverterTest.kt | 2 +- 19 files changed, 196 insertions(+), 223 deletions(-) delete mode 100644 ultrasonic/src/main/res/layout/video_details.xml delete mode 100644 ultrasonic/src/main/res/layout/video_list_item.xml diff --git a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAlbumListRequestTest.kt b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAlbumListRequestTest.kt index 37ee2f90..b580ad6d 100644 --- a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAlbumListRequestTest.kt +++ b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAlbumListRequestTest.kt @@ -2,9 +2,9 @@ package org.moire.ultrasonic.api.subsonic import org.amshove.kluent.`should be equal to` import org.junit.Test +import org.moire.ultrasonic.api.subsonic.models.Album import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.api.subsonic.models.AlbumListType.BY_GENRE -import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild /** * Integration tests for [SubsonicAPIDefinition] for getAlbumList call. @@ -28,8 +28,8 @@ class SubsonicApiGetAlbumListRequestTest : SubsonicAPIClientTest() { assertResponseSuccessful(response) with(response.body()!!.albumList) { size `should be equal to` 2 - this[1] `should be equal to` MusicDirectoryChild( - id = "9997", parent = "9996", isDir = true, + this[1] `should be equal to` Album( + id = "9997", parent = "9996", title = "Endless Forms Most Beautiful", album = "Endless Forms Most Beautiful", artist = "Nightwish", year = 2015, genre = "Symphonic Metal", coverArt = "9997", playCount = 11, diff --git a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiSearchTwoTest.kt b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiSearchTwoTest.kt index 9ab6b79b..17cb3ef0 100644 --- a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiSearchTwoTest.kt +++ b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiSearchTwoTest.kt @@ -3,6 +3,7 @@ package org.moire.ultrasonic.api.subsonic import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should not be` import org.junit.Test +import org.moire.ultrasonic.api.subsonic.models.Album import org.moire.ultrasonic.api.subsonic.models.Artist import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult @@ -32,9 +33,8 @@ class SubsonicApiSearchTwoTest : SubsonicAPIClientTest() { artistList.size `should be equal to` 1 artistList[0] `should be equal to` Artist(id = "522", name = "The Prodigy") albumList.size `should be equal to` 1 - albumList[0] `should be equal to` MusicDirectoryChild( - id = "8867", parent = "522", - isDir = true, title = "Always Outnumbered, Never Outgunned", + albumList[0] `should be equal to` Album( + id = "8867", parent = "522", title = "Always Outnumbered, Never Outgunned", album = "Always Outnumbered, Never Outgunned", artist = "The Prodigy", year = 2004, genre = "Electronic", coverArt = "8867", playCount = 0, created = parseDate("2016-10-23T20:57:27.000Z") diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Album.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Album.kt index 47ad7cd8..f326caae 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Album.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Album.kt @@ -18,6 +18,7 @@ data class Album( val duration: Int = 0, val year: Int = 0, val genre: String = "", + val playCount: Int = 0, @JsonProperty("song") val songList: List = emptyList(), @JsonProperty("starred") val starredDate: String = "" ) diff --git a/detekt-config.yml b/detekt-config.yml index 8729b6e3..2bb90612 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -42,8 +42,8 @@ empty-blocks: complexity: active: true TooManyFunctions: - thresholdInFiles: 20 - thresholdInClasses: 20 + thresholdInFiles: 25 + thresholdInClasses: 25 thresholdInInterfaces: 20 thresholdInObjects: 30 LabeledExpression: diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index 46313ca4..82d40231 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -1,20 +1,6 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -1086,17 +1105,6 @@ column="14"/> - - - - @@ -1502,17 +1510,6 @@ column="10"/> - - - - : MultiTypeAdapter() { fun notifyChanged() { // When the download state of an entry was changed by an external process, // increase the revision counter in order to update the UI - selectionRevision.postValue(selectionRevision.value!! + 1) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt index 6e862163..eae411dd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt @@ -16,7 +16,7 @@ class DividerBinder : ItemViewBinder - onContextMenuClick?.invoke(menuItem, downloadFile) + onContextMenuClick.invoke(menuItem, downloadFile) } } else { // Minimize or maximize the Text view (if song title is very long) @@ -78,22 +81,22 @@ class TrackViewBinder( } holder.itemView.setOnClickListener { - if (!checkable) { - onItemClick(downloadFile) - } else { + if (checkable && !downloadFile.song.isVideo) { val nowChecked = !holder.check.isChecked holder.isChecked = nowChecked + } else { + onItemClick(downloadFile) } } // Notify the adapter of selection changes holder.observableChecked.observe( lifecycleOwner, - { newValue -> - if (newValue) { - diffAdapter.notifySelected(item.longId) + { isCheckedNow -> + if (isCheckedNow) { + diffAdapter.notifySelected(holder.entry!!.longId) } else { - diffAdapter.notifyUnselected(item.longId) + diffAdapter.notifyUnselected(holder.entry!!.longId) } } ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index a0e98132..fc5ba282 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -28,13 +28,11 @@ import timber.log.Timber /** * Used to display songs and videos in a `ListView`. - * FIXME: Add video List item - * FIXME: CHECKED bug */ class TrackViewHolder(val view: View) : 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 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) @@ -90,7 +88,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable } check.isVisible = (checkable && !song.isVideo) - setCheckedSilent(isSelected) + initChecked(isSelected) drag.isVisible = draggable if (ActiveServerProvider.isOffline()) { @@ -109,6 +107,11 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable setSingleStar(entry!!.starred) } + if (song.isVideo) { + artist.isVisible = false + progress.isVisible = false + } + RxBus.playerStateObservable.subscribe { setPlayIcon(it.track == downloadFile) } @@ -248,14 +251,23 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable } } - private fun setCheckedSilent(newStatus: Boolean) { + /* + * Set the checked value and re-init the MutableLiveData. + * If we would post a new value, there might be a short glitch where the track is shown with its + * old selection status before the posted value has been processed. + */ + private fun initChecked(newStatus: Boolean) { + observableChecked = MutableLiveData(newStatus) check.isChecked = newStatus } + /* + * To be correct, this method doesn't directly set the checked status. + * It only notifies the observable. If the selection tracker accepts the selection + * (might be false for Singular SelectionTrackers) then it will cause the actual modification. + */ override fun setChecked(newStatus: Boolean) { observableChecked.postValue(newStatus) - // FIXME, check if working - // check.isChecked = newStatus } override fun isChecked(): Boolean { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index f11ab127..342e6a87 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -43,10 +43,10 @@ class AlbumListFragment : EntryListFragment() { ): LiveData> { if (args == null) throw IllegalArgumentException("Required arguments are missing") - val refresh = args.getBoolean(Constants.INTENT_REFRESH) || refresh + val refresh2 = args.getBoolean(Constants.INTENT_REFRESH) || refresh val append = args.getBoolean(Constants.INTENT_APPEND) - return listModel.getAlbumList(refresh or append, refreshListView!!, args) + return listModel.getAlbumList(refresh2 or append, refreshListView!!, args) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 10090b65..2446e05a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -33,8 +33,8 @@ class ArtistListFragment : EntryListFragment() { * The central function to pass a query to the model and return a LiveData object */ override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { - val refresh = args?.getBoolean(Constants.INTENT_REFRESH) ?: false || refresh - return listModel.getItems(refresh, refreshListView!!) + val refresh2 = args?.getBoolean(Constants.INTENT_REFRESH) ?: false || refresh + return listModel.getItems(refresh2, refreshListView!!) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt index 8f575ce7..c9c8b6b9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -34,7 +34,10 @@ class BookmarksFragment : TrackCollectionFragment() { viewAdapter.selectionType = BaseAdapter.SelectionType.SINGLE } - override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData> { + override fun getLiveData( + args: Bundle?, + refresh: Boolean + ): LiveData> { listModel.viewModelScope.launch(handler) { refreshListView?.isRefreshing = true listModel.getBookmarks() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 37451286..86a62493 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -868,7 +868,8 @@ class PlayerFragment : ) dragTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback( - ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0 + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT ) { override fun onMove( @@ -887,7 +888,20 @@ class PlayerFragment : return true } + // Swipe to delete from playlist override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val pos = viewHolder.bindingAdapterPosition + val file = mediaPlayerController.playList[pos] + mediaPlayerController.removeFromPlaylist(file) + + val songRemoved = String.format( + resources.getString(R.string.download_song_removed), + file.song.title + ) + Util.toast(context, songRemoved) + + viewAdapter.submitList(mediaPlayerController.playList) + viewAdapter.notifyDataSetChanged() } } ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 073742e1..cd6b13c6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -120,7 +120,7 @@ class Downloader( } @Synchronized - @Suppress("ComplexMethod") + @Suppress("ComplexMethod", "ComplexCondition") fun checkDownloadsInternal() { if ( !Util.isExternalStoragePresent() || @@ -502,7 +502,7 @@ class Downloader( /** * Extension function - * Gathers the donwload file for a given song, and modifies shouldSave if provided. + * Gathers the download file for a given song, and modifies shouldSave if provided. */ fun MusicDirectory.Entry.getDownloadFile(save: Boolean? = null): DownloadFile { return getDownloadFileForSong(this).apply { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 8859a272..55bffc4c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -299,6 +299,7 @@ class MediaPlayerController( } @Synchronized + // TODO: If a playlist contains an item twice, this call will wrongly remove all fun removeFromPlaylist(downloadFile: DownloadFile) { if (downloadFile == localMediaPlayer.currentPlaying) { reset() diff --git a/ultrasonic/src/main/res/layout/list_item_track_details.xml b/ultrasonic/src/main/res/layout/list_item_track_details.xml index 2b592b0a..a224ab15 100644 --- a/ultrasonic/src/main/res/layout/list_item_track_details.xml +++ b/ultrasonic/src/main/res/layout/list_item_track_details.xml @@ -1,74 +1,84 @@ - + a:layout_weight="1"> - + a:paddingEnd="6dip" + a:textAppearance="?android:attr/textAppearanceMedium" + app:layout_constraintBottom_toTopOf="@+id/song_artist" + app:layout_constraintEnd_toStartOf="@+id/song_title" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" + tools:text="Track" /> - - - - - - - - + a:drawablePadding="4dip" + a:ellipsize="end" + a:paddingEnd="4dip" + a:singleLine="true" + a:textAppearance="?android:attr/textAppearanceMedium" + app:layout_constraintBottom_toTopOf="@+id/song_artist" + app:layout_constraintEnd_toStartOf="@+id/song_duration" + app:layout_constraintStart_toEndOf="@+id/song_track" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" + tools:text="Title" /> - + - - - \ No newline at end of file + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/video_details.xml b/ultrasonic/src/main/res/layout/video_details.xml deleted file mode 100644 index 2ea9473c..00000000 --- a/ultrasonic/src/main/res/layout/video_details.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/video_list_item.xml b/ultrasonic/src/main/res/layout/video_list_item.xml deleted file mode 100644 index 5851bbca..00000000 --- a/ultrasonic/src/main/res/layout/video_list_item.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APISearchConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APISearchConverterTest.kt index 02159012..ff3b30f8 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APISearchConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APISearchConverterTest.kt @@ -42,7 +42,7 @@ class APISearchConverterTest { fun `Should convert SearchTwoResult to domain entity`() { val entity = SearchTwoResult( listOf(Artist(id = "82", name = "great-artist-name")), - listOf(MusicDirectoryChild(id = "762", artist = "bzz")), + listOf(Album(id = "762", artist = "bzz")), listOf(MusicDirectoryChild(id = "9118", parent = "112")) ) From 6daa17efd5cc0e2340bf049cb8af096c1f0dc380 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 5 Dec 2021 14:05:42 +0100 Subject: [PATCH 24/33] Show folder header in Artist list --- .../main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt | 1 - .../main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt | 4 ++++ .../kotlin/org/moire/ultrasonic/model/GenericListModel.kt | 1 - 3 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt deleted file mode 100644 index 5a3abd0b..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ImageHelper.kt +++ /dev/null @@ -1 +0,0 @@ -package org.moire.ultrasonic.adapters diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt index 8861a9ef..6c2de732 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt @@ -67,6 +67,10 @@ class ArtistListModel(application: Application) : GenericListModel(application) artists.postValue(result.toMutableList().sortedWith(comparator)) } + override fun showSelectFolderHeader(args: Bundle?): Boolean { + return true + } + companion object { val comparator: Comparator = compareBy(Collator.getInstance()) { t -> t.name } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt index c03880fe..2d4b4aea 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt @@ -41,7 +41,6 @@ open class GenericListModel(application: Application) : val musicFolders: MutableLiveData> = MutableLiveData(listOf()) - @Suppress("UNUSED_PARAMETER") open fun showSelectFolderHeader(args: Bundle?): Boolean { return false } From 026aa79572c26e9d9e354e92caf06ffceba9c8bb Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 5 Dec 2021 21:07:08 +0100 Subject: [PATCH 25/33] Remove special casing of SongsForGenre and thereby fix it. Also prevent jumping in the random albums list and don't refresh the album list on back navigation --- .../ultrasonic/fragment/AlbumListFragment.kt | 5 ++ .../ultrasonic/fragment/EntryListFragment.kt | 2 +- .../ultrasonic/fragment/MultiListFragment.kt | 9 +- .../fragment/TrackCollectionFragment.kt | 83 +++++++++---------- .../moire/ultrasonic/model/AlbumListModel.kt | 8 +- .../ultrasonic/model/TrackCollectionModel.kt | 3 +- 6 files changed, 60 insertions(+), 50 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 342e6a87..93c7d1c7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -34,6 +34,11 @@ class AlbumListFragment : EntryListFragment() { */ override val mainLayout: Int = R.layout.list_layout_generic + /** + * Whether to refresh the data onViewCreated + */ + override val refreshOnCreation: Boolean = false + /** * The central function to pass a query to the model and return a LiveData object */ diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 22942703..395d2d6c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -71,7 +71,7 @@ abstract class EntryListFragment : MultiListFragment() { * What to do when the list has changed */ override val defaultObserver: (List) -> Unit = { - emptyView.isVisible = it.isEmpty() + emptyView.isVisible = it.isEmpty() && !(refreshListView?.isRefreshing?:false) if (showFolderHeader()) { val list = mutableListOf(folderHeader) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index 0c819730..fef74587 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -89,6 +89,11 @@ abstract class MultiListFragment : Fragment() { open val emptyViewId = R.id.empty_list_view open val emptyTextId = R.id.empty_list_text + /** + * Whether to refresh the data onViewCreated + */ + open val refreshOnCreation: Boolean = true + open fun setTitle(title: String?) { if (title == null) { FragmentTitle.setTitle( @@ -106,7 +111,7 @@ abstract class MultiListFragment : Fragment() { * What to do when the list has changed */ internal open val defaultObserver: ((List) -> Unit) = { - emptyView.isVisible = it.isEmpty() + emptyView.isVisible = it.isEmpty() && !(refreshListView?.isRefreshing?:false) viewAdapter.submitList(it) } @@ -123,7 +128,7 @@ abstract class MultiListFragment : Fragment() { } // Populate the LiveData. This starts an API request in most cases - liveDataItems = getLiveData(arguments, true) + liveDataItems = getLiveData(arguments, refreshOnCreation) // Link view to display text if the list is empty emptyView = view.findViewById(emptyViewId) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 8e4dc778..ca9b099e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -18,7 +18,6 @@ import android.widget.ImageView 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.recyclerview.widget.LinearLayoutManager @@ -54,6 +53,8 @@ import org.moire.ultrasonic.util.Util * In most cases the data should be just a list of Entries, but there are some cases * where the list can contain Albums as well. This happens especially when having ID3 tags disabled, * or using Offline mode, both in which Indexes instead of Artists are being used. + * + * TODO: Remove more button and introduce endless scrolling */ @Suppress("TooManyFunctions") open class TrackCollectionFragment : MultiListFragment() { @@ -97,9 +98,6 @@ open class TrackCollectionFragment : MultiListFragment() { getLiveData(arguments, true) } - // TODO: remove special casing for songsForGenre - listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver) - setupButtons(view) registerForContextMenu(listView!!) @@ -424,34 +422,6 @@ open class TrackCollectionFragment : MultiListFragment() { mediaPlayerController.unpin(songs) } - private val songsForGenreObserver = Observer { musicDirectory -> - - // Hide more button when results are less than album list size - if (musicDirectory.size < requireArguments().getInt( - Constants.INTENT_ALBUM_LIST_SIZE, 0 - ) - ) { - moreButton!!.visibility = View.GONE - } else { - moreButton!!.visibility = View.VISIBLE - } - - moreButton!!.setOnClickListener { - val theGenre = requireArguments().getString(Constants.INTENT_GENRE_NAME) - val size = requireArguments().getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) - val theOffset = requireArguments().getInt( - Constants.INTENT_ALBUM_LIST_OFFSET, 0 - ) + size - val bundle = Bundle() - bundle.putString(Constants.INTENT_GENRE_NAME, theGenre) - bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, size) - bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, theOffset) - - Navigation.findNavController(requireView()) - .navigate(R.id.trackCollectionFragment, bundle) - } - } - override val defaultObserver: (List) -> Unit = { val entryList: MutableList = it.toMutableList() @@ -483,18 +453,9 @@ open class TrackCollectionFragment : MultiListFragment() { } else { moreButton!!.visibility = View.VISIBLE if (arguments?.getInt(Constants.INTENT_RANDOM, 0) ?: 0 > 0) { - moreButton!!.setOnClickListener { - val offset = requireArguments().getInt( - Constants.INTENT_ALBUM_LIST_OFFSET, 0 - ) + listSize - val bundle = Bundle() - bundle.putInt(Constants.INTENT_RANDOM, 1) - bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, listSize) - bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, offset) - Navigation.findNavController(requireView()).navigate( - R.id.trackCollectionFragment, bundle - ) - } + moreRandomTracks() + } else if (arguments?.getString(Constants.INTENT_GENRE_NAME, "") ?: "" != "") { + moreSongsForGenre() } } } @@ -536,6 +497,40 @@ open class TrackCollectionFragment : MultiListFragment() { listModel.currentListIsSortable = true } + private fun moreSongsForGenre(args: Bundle = requireArguments()) { + moreButton!!.setOnClickListener { + val theGenre = args.getString(Constants.INTENT_GENRE_NAME) + val size = args.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) + val theOffset = args.getInt( + Constants.INTENT_ALBUM_LIST_OFFSET, 0 + ) + size + val bundle = Bundle() + bundle.putString(Constants.INTENT_GENRE_NAME, theGenre) + bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, size) + bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, theOffset) + + Navigation.findNavController(requireView()) + .navigate(R.id.trackCollectionFragment, bundle) + } + } + + private fun moreRandomTracks() { + val listSize = arguments?.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) ?: 0 + + moreButton!!.setOnClickListener { it: View? -> + val offset = requireArguments().getInt( + Constants.INTENT_ALBUM_LIST_OFFSET, 0 + ) + listSize + val bundle = Bundle() + bundle.putInt(Constants.INTENT_RANDOM, 1) + bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, listSize) + bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, offset) + Navigation.findNavController(requireView()).navigate( + R.id.trackCollectionFragment, bundle + ) + } + } + internal fun getSelectedSongs(): List { // Walk through selected set and get the Entries based on the saved ids. return viewAdapter.getCurrentList().mapNotNull { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt index 816696c7..5fd2e86d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -63,6 +63,12 @@ class AlbumListModel(application: Application) : GenericListModel(application) { null } + // If we are refreshing the random list, we want to avoid items moving across the screen, + // by clearing the list first + if (refresh && albumListType == "random") { + list.postValue(listOf()) + } + // Handle the logic for endless scrolling: // If appending the existing list, set the offset from where to load if (append) offset += (size + loadedUntil) @@ -95,7 +101,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) { val newList = ArrayList() newList.addAll(list.value!!) newList.addAll(musicDirectory) - this.list.postValue(newList) + list.postValue(newList) } else { list.postValue(musicDirectory) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index 3b658ae6..8fdd1202 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -22,7 +22,6 @@ import org.moire.ultrasonic.util.Util class TrackCollectionModel(application: Application) : GenericListModel(application) { val currentList: MutableLiveData> = MutableLiveData() - val songsForGenre: MutableLiveData = MutableLiveData() /* * Especially when dealing with indexes, this method can return Albums, Entries or a mix of both! @@ -56,7 +55,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val musicDirectory = service.getSongsByGenre(genre, count, offset) - songsForGenre.postValue(musicDirectory) + updateList(musicDirectory) } } From de04f4cbe6f3aa6d8326f62de8dc08db1f5f0367 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 5 Dec 2021 21:29:32 +0100 Subject: [PATCH 26/33] Fix the alignment of the status text, add transparency when dragging a song, remove wrong context menu --- .../ultrasonic/fragment/PlayerFragment.kt | 22 ++++++++++++++++++- .../src/main/res/layout/list_item_track.xml | 19 ++++++++++------ .../res/layout/list_item_track_details.xml | 1 - 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 86a62493..7e26c289 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -37,6 +37,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.navigation.Navigation import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView @@ -859,7 +860,6 @@ class PlayerFragment : viewAdapter.register( TrackViewBinder( onItemClick = listener, - onContextMenuClick = { _, _ -> true }, checkable = false, draggable = true, context = requireContext(), @@ -903,6 +903,26 @@ class PlayerFragment : viewAdapter.submitList(mediaPlayerController.playList) viewAdapter.notifyDataSetChanged() } + + override fun onSelectedChanged( + viewHolder: RecyclerView.ViewHolder?, + actionState: Int + ) { + super.onSelectedChanged(viewHolder, actionState) + + if (actionState == ACTION_STATE_DRAG) { + viewHolder?.itemView?.alpha = 0.6f + } + } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ) { + super.clearView(recyclerView, viewHolder) + + viewHolder.itemView.alpha = 1.0f + } } ) diff --git a/ultrasonic/src/main/res/layout/list_item_track.xml b/ultrasonic/src/main/res/layout/list_item_track.xml index c2c140af..1ac6d686 100644 --- a/ultrasonic/src/main/res/layout/list_item_track.xml +++ b/ultrasonic/src/main/res/layout/list_item_track.xml @@ -10,13 +10,13 @@ a:id="@+id/song_drag" a:layout_width="wrap_content" a:layout_height="fill_parent" - a:paddingStart="5dip" - a:paddingEnd="0dip" a:background="@android:color/transparent" a:focusable="false" a:gravity="center_vertical" - a:src="?attr/drag_vertical" - a:importantForAccessibility="no" /> + a:importantForAccessibility="no" + a:paddingStart="5dip" + a:paddingEnd="0dip" + a:src="?attr/drag_vertical" /> + a:paddingEnd="4dip" /> @@ -43,6 +43,7 @@ a:background="@android:color/transparent" a:focusable="false" a:gravity="center_vertical" + a:importantForAccessibility="no" a:scaleType="centerInside" a:src="?attr/star_hollow" /> @@ -53,6 +54,7 @@ a:background="@android:color/transparent" a:focusable="false" a:gravity="center_vertical" + a:importantForAccessibility="no" a:scaleType="centerInside" a:src="?attr/star_hollow" /> @@ -63,6 +65,7 @@ a:background="@android:color/transparent" a:focusable="false" a:gravity="center_vertical" + a:importantForAccessibility="no" a:scaleType="centerInside" a:src="?attr/star_hollow" /> @@ -73,6 +76,7 @@ a:background="@android:color/transparent" a:focusable="false" a:gravity="center_vertical" + a:importantForAccessibility="no" a:scaleType="centerInside" a:src="?attr/star_hollow" /> @@ -84,6 +88,7 @@ a:background="@android:color/transparent" a:focusable="false" a:gravity="center_vertical" + a:importantForAccessibility="no" a:scaleType="centerInside" a:src="?attr/star_hollow" /> @@ -94,10 +99,10 @@ a:layout_width="38dp" a:layout_height="fill_parent" a:background="@android:color/transparent" + a:contentDescription="@string/download.menu_star" a:focusable="false" a:gravity="center_vertical" a:paddingEnd="8dip" - a:src="?attr/star_hollow" - a:contentDescription="@string/download.menu_star"/> + a:src="?attr/star_hollow" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/list_item_track_details.xml b/ultrasonic/src/main/res/layout/list_item_track_details.xml index a224ab15..a2da6642 100644 --- a/ultrasonic/src/main/res/layout/list_item_track_details.xml +++ b/ultrasonic/src/main/res/layout/list_item_track_details.xml @@ -46,7 +46,6 @@ a:drawablePadding="6dip" a:paddingEnd="12dip" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/song_title" app:layout_constraintTop_toTopOf="parent" tools:text="100%" /> From 0d24c87eefb09baef02e8c1128ef4850537f58f7 Mon Sep 17 00:00:00 2001 From: Nite Date: Mon, 6 Dec 2021 19:23:22 +0100 Subject: [PATCH 27/33] - Fixed track item layout when track number is missing - Fixed Rx unsubscribing - Fixed drag handle usage in playlist --- .../ultrasonic/adapters/TrackViewBinder.kt | 19 +++++++++++++++++++ .../ultrasonic/adapters/TrackViewHolder.kt | 9 ++++++++- .../ultrasonic/fragment/PlayerFragment.kt | 8 +++++++- .../res/layout/list_item_track_details.xml | 6 ++++-- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index 691eeee1..38903da6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -1,9 +1,13 @@ package org.moire.ultrasonic.adapters +import android.annotation.SuppressLint import android.content.Context import android.view.LayoutInflater import android.view.MenuItem +import android.view.MotionEvent +import android.view.View import android.view.ViewGroup +import androidx.core.view.MotionEventCompat import androidx.lifecycle.LifecycleOwner import com.drakeet.multitype.ItemViewBinder import org.koin.core.component.KoinComponent @@ -23,6 +27,8 @@ class TrackViewBinder( val lifecycleOwner: LifecycleOwner, ) : ItemViewBinder(), KoinComponent { + var startDrag: ((TrackViewHolder) -> Unit)? = null + // Set our layout files val layout = R.layout.list_item_track val contextMenuLayout = R.menu.context_menu_track @@ -34,6 +40,7 @@ class TrackViewBinder( return TrackViewHolder(inflater.inflate(layout, parent, false)) } + @SuppressLint("ClickableViewAccessibility") @Suppress("LongMethod") override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) { val downloadFile: DownloadFile? @@ -89,6 +96,13 @@ class TrackViewBinder( } } + holder.drag.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + startDrag?.invoke(holder) + } + false + } + // Notify the adapter of selection changes holder.observableChecked.observe( lifecycleOwner, @@ -127,4 +141,9 @@ class TrackViewBinder( } ) } + + override fun onViewRecycled(holder: TrackViewHolder) { + holder.dispose() + super.onViewRecycled(holder) + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index fc5ba282..ee46f7dc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -11,6 +11,7 @@ import android.widget.TextView import androidx.core.view.isVisible import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.RecyclerView +import io.reactivex.rxjava3.disposables.Disposable import org.koin.core.component.KoinComponent import org.koin.core.component.get import org.moire.ultrasonic.R @@ -56,6 +57,8 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable private var statusImage: Drawable? = null private var isPlayingCached = false + private var rxSubscription: Disposable? = null + var observableChecked = MutableLiveData(false) private val useFiveStarRating: Boolean by lazy { @@ -112,11 +115,15 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable progress.isVisible = false } - RxBus.playerStateObservable.subscribe { + rxSubscription = RxBus.playerStateObservable.subscribe { setPlayIcon(it.track == downloadFile) } } + fun dispose() { + rxSubscription?.dispose() + } + private fun setPlayIcon(isPlaying: Boolean) { if (isPlaying && !isPlayingCached) { isPlayingCached = true diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 7e26c289..3c22a01b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -864,7 +864,9 @@ class PlayerFragment : draggable = true, context = requireContext(), lifecycleOwner = viewLifecycleOwner, - ) + ).apply { this.startDrag = { holder -> + dragTouchHelper.startDrag(holder) + } } ) dragTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback( @@ -923,6 +925,10 @@ class PlayerFragment : viewHolder.itemView.alpha = 1.0f } + + override fun isLongPressDragEnabled(): Boolean { + return false + } } ) diff --git a/ultrasonic/src/main/res/layout/list_item_track_details.xml b/ultrasonic/src/main/res/layout/list_item_track_details.xml index a2da6642..62473b67 100644 --- a/ultrasonic/src/main/res/layout/list_item_track_details.xml +++ b/ultrasonic/src/main/res/layout/list_item_track_details.xml @@ -16,12 +16,14 @@ a:layout_height="wrap_content" a:paddingEnd="6dip" a:textAppearance="?android:attr/textAppearanceMedium" + a:visibility="visible" app:layout_constraintBottom_toTopOf="@+id/song_artist" app:layout_constraintEnd_toStartOf="@+id/song_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" - tools:text="Track" /> + tools:text="Track" + tools:visibility="visible" /> Date: Mon, 6 Dec 2021 23:37:54 +0100 Subject: [PATCH 28/33] Fix constraints in track details --- .../res/layout/list_item_track_details.xml | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/ultrasonic/src/main/res/layout/list_item_track_details.xml b/ultrasonic/src/main/res/layout/list_item_track_details.xml index 62473b67..569a64f7 100644 --- a/ultrasonic/src/main/res/layout/list_item_track_details.xml +++ b/ultrasonic/src/main/res/layout/list_item_track_details.xml @@ -35,23 +35,13 @@ a:singleLine="true" a:textAppearance="?android:attr/textAppearanceMedium" app:layout_constraintBottom_toTopOf="@+id/song_artist" - app:layout_constraintEnd_toStartOf="@+id/song_duration" + app:layout_constraintEnd_toStartOf="@+id/barrier" app:layout_constraintStart_toEndOf="@+id/song_track" app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="1.0" app:layout_constraintVertical_chainStyle="packed" tools:text="Title" /> - - - + + + + \ No newline at end of file From d8cdc8142446e5db6a2f70c1afc1f742e28b7633 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 7 Dec 2021 00:04:53 +0100 Subject: [PATCH 29/33] Increase touch area of drag handler. Also use new tintable color for drag drawable --- ultrasonic/src/main/res/drawable/ic_drag_vertical.xml | 10 ++++++++++ .../src/main/res/drawable/ic_drag_vertical_dark.xml | 8 -------- .../src/main/res/drawable/ic_drag_vertical_light.xml | 8 -------- ultrasonic/src/main/res/layout/list_item_track.xml | 4 ++-- ultrasonic/src/main/res/values/themes.xml | 3 --- 5 files changed, 12 insertions(+), 21 deletions(-) create mode 100644 ultrasonic/src/main/res/drawable/ic_drag_vertical.xml delete mode 100644 ultrasonic/src/main/res/drawable/ic_drag_vertical_dark.xml delete mode 100644 ultrasonic/src/main/res/drawable/ic_drag_vertical_light.xml diff --git a/ultrasonic/src/main/res/drawable/ic_drag_vertical.xml b/ultrasonic/src/main/res/drawable/ic_drag_vertical.xml new file mode 100644 index 00000000..5aa2d20c --- /dev/null +++ b/ultrasonic/src/main/res/drawable/ic_drag_vertical.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/drawable/ic_drag_vertical_dark.xml b/ultrasonic/src/main/res/drawable/ic_drag_vertical_dark.xml deleted file mode 100644 index 8dcc63e5..00000000 --- a/ultrasonic/src/main/res/drawable/ic_drag_vertical_dark.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/ultrasonic/src/main/res/drawable/ic_drag_vertical_light.xml b/ultrasonic/src/main/res/drawable/ic_drag_vertical_light.xml deleted file mode 100644 index 7fdf1d55..00000000 --- a/ultrasonic/src/main/res/drawable/ic_drag_vertical_light.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/list_item_track.xml b/ultrasonic/src/main/res/layout/list_item_track.xml index 1ac6d686..16322ac7 100644 --- a/ultrasonic/src/main/res/layout/list_item_track.xml +++ b/ultrasonic/src/main/res/layout/list_item_track.xml @@ -15,8 +15,8 @@ a:gravity="center_vertical" a:importantForAccessibility="no" a:paddingStart="5dip" - a:paddingEnd="0dip" - a:src="?attr/drag_vertical" /> + a:paddingEnd="6dip" + a:src="@drawable/ic_drag_vertical" /> @drawable/ic_subdirectory_up_dark @drawable/ic_sd_storage_dark @drawable/ic_drag_queue_dark - @drawable/ic_drag_vertical_dark @drawable/ic_more_vert_dark @drawable/list_selector_holo_dark @drawable/list_selector_holo_dark_selected @@ -122,7 +121,6 @@ @drawable/ic_subdirectory_up_dark @drawable/ic_sd_storage_dark @drawable/ic_drag_queue_dark - @drawable/ic_drag_vertical_dark @drawable/ic_more_vert_dark @drawable/list_selector_holo_dark @drawable/list_selector_holo_dark_selected @@ -186,7 +184,6 @@ @drawable/ic_subdirectory_up_light @drawable/ic_sd_storage_light @drawable/ic_drag_queue_light - @drawable/ic_drag_vertical_light @drawable/ic_more_vert_light @drawable/list_selector_holo_light @drawable/list_selector_holo_light_selected From e3371777154ce1b804a4110a6e4c12255ec11775 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 7 Dec 2021 00:06:41 +0100 Subject: [PATCH 30/33] Style fixes --- .../org/moire/ultrasonic/adapters/TrackViewBinder.kt | 2 -- .../org/moire/ultrasonic/fragment/PlayerFragment.kt | 8 +++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index 38903da6..2ed77e52 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -5,9 +5,7 @@ import android.content.Context import android.view.LayoutInflater import android.view.MenuItem import android.view.MotionEvent -import android.view.View import android.view.ViewGroup -import androidx.core.view.MotionEventCompat import androidx.lifecycle.LifecycleOwner import com.drakeet.multitype.ItemViewBinder import org.koin.core.component.KoinComponent diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 3c22a01b..2b2c61d3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -864,9 +864,11 @@ class PlayerFragment : draggable = true, context = requireContext(), lifecycleOwner = viewLifecycleOwner, - ).apply { this.startDrag = { holder -> - dragTouchHelper.startDrag(holder) - } } + ).apply { + this.startDrag = { holder -> + dragTouchHelper.startDrag(holder) + } + } ) dragTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback( From 80e587c1aa0c3dd39d55bd12b9cd99731ef0de57 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 8 Dec 2021 17:51:31 +0100 Subject: [PATCH 31/33] Add scrollbar to playlist view, implement SectionedAdapter for Artists --- .../ultrasonic/adapters/ArtistRowBinder.kt | 16 ++++++++-- .../moire/ultrasonic/adapters/BaseAdapter.kt | 14 ++++++++- .../org/moire/ultrasonic/adapters/Utils.kt | 5 ++++ .../fragment/TrackCollectionFragment.kt | 2 +- .../src/main/res/layout/current_playlist.xml | 29 +++++++++++++------ 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt index 4a38f667..d83385ab 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt @@ -20,6 +20,7 @@ import com.drakeet.multitype.ItemViewBinder import org.koin.core.component.KoinComponent import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.ArtistOrIndex +import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Settings @@ -32,14 +33,16 @@ class ArtistRowBinder( val onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean, private val imageLoader: ImageLoader, private val enableSections: Boolean = true -) : ItemViewBinder(), KoinComponent { +) : ItemViewBinder(), + KoinComponent, + Utils.SectionedBinder { val layout = R.layout.list_item_artist val contextMenuLayout = R.menu.context_menu_artist override fun onBindViewHolder(holder: ViewHolder, item: ArtistOrIndex) { holder.textView.text = item.name - holder.section.text = getSectionForArtist(item) + holder.section.text = getSectionForDisplay(item) holder.section.isVisible = enableSections holder.layout.setOnClickListener { onItemClick(item) } holder.layout.setOnLongClickListener { @@ -70,7 +73,14 @@ class ArtistRowBinder( } } - private fun getSectionForArtist(item: ArtistOrIndex): String { + override fun getSectionName(item: Identifiable): String { + val index = adapter.items.indexOf(item) + if (index == -1 || item !is ArtistOrIndex) return "" + + return getSectionFromName(item.name ?: "") + } + + private fun getSectionForDisplay(item: ArtistOrIndex): String { val index = adapter.items.indexOf(item) if (index == -1) return " " diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index d4e9da09..55f17f0c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -15,6 +15,7 @@ import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer.ListListener import androidx.recyclerview.widget.DiffUtil import com.drakeet.multitype.MultiTypeAdapter +import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.util.BoundedTreeSet import timber.log.Timber @@ -26,7 +27,7 @@ import timber.log.Timber * It should be kept generic enough that it can be used a Base for all lists in the app. */ @Suppress("unused", "UNUSED_PARAMETER") -class BaseAdapter : MultiTypeAdapter() { +class BaseAdapter : MultiTypeAdapter(), FastScrollRecyclerView.SectionedAdapter { // Update the BoundedTreeSet if selection type is changed internal var selectionType: SelectionType = SelectionType.MULTIPLE @@ -221,4 +222,15 @@ class BaseAdapter : MultiTypeAdapter() { return oldItem.id == newItem.id } } + + override fun getSectionName(position: Int): String { + val type = getItemViewType(position) + val binder = types.getType(type).delegate + + if (binder is Utils.SectionedBinder) { + return binder.getSectionName(items[position] as Identifiable) + } + + return "" + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Utils.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Utils.kt index e1ea9093..991ae445 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Utils.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Utils.kt @@ -7,6 +7,7 @@ import android.view.View import android.widget.PopupMenu import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util @@ -69,4 +70,8 @@ object Utils { playingImage = Util.getDrawableFromAttribute(context, R.attr.media_play_small) } } + + interface SectionedBinder { + fun getSectionName(item: Identifiable): String + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index ca9b099e..e9f398e2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -517,7 +517,7 @@ open class TrackCollectionFragment : MultiListFragment() { private fun moreRandomTracks() { val listSize = arguments?.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) ?: 0 - moreButton!!.setOnClickListener { it: View? -> + moreButton!!.setOnClickListener { val offset = requireArguments().getInt( Constants.INTENT_ALBUM_LIST_OFFSET, 0 ) + listSize diff --git a/ultrasonic/src/main/res/layout/current_playlist.xml b/ultrasonic/src/main/res/layout/current_playlist.xml index 3e6eeafb..bad74a52 100644 --- a/ultrasonic/src/main/res/layout/current_playlist.xml +++ b/ultrasonic/src/main/res/layout/current_playlist.xml @@ -1,23 +1,34 @@ - - + a:layout_height="fill_parent" + a:orientation="vertical"> + a:padding="10dip" + a:text="@string/playlist.empty" /> - + a:clipToPadding="false" + a:paddingTop="8dp" + a:paddingBottom="8dp" + app:fastScrollAutoHide="true" + app:fastScrollAutoHideDelay="2000" + app:fastScrollPopupBackgroundSize="42dp" + app:fastScrollPopupBgColor="@color/cyan" + app:fastScrollPopupPosition="adjacent" + app:fastScrollPopupTextColor="@android:color/primary_text_dark" + app:fastScrollPopupTextSize="28sp" + app:fastScrollThumbColor="@color/cyan" + app:fastScrollTrackColor="@color/dividerColor" /> \ No newline at end of file From fb85fb4e82eb296d7d0f4c11eb6a0d33ea04d983 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Dec 2021 18:07:14 +0000 Subject: [PATCH 32/33] Bump versions.mockito from 4.0.0 to 4.1.0 Bumps `versions.mockito` from 4.0.0 to 4.1.0. Updates `mockito-core` from 4.0.0 to 4.1.0 - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.0.0...v4.1.0) Updates `mockito-inline` from 4.0.0 to 4.1.0 - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.0.0...v4.1.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.mockito:mockito-inline dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index fffa284b..7903647b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -33,7 +33,7 @@ ext.versions = [ junit4 : "4.13.2", junit5 : "5.8.1", - mockito : "4.0.0", + mockito : "4.1.0", mockitoKotlin : "4.0.0", kluent : "1.68", apacheCodecs : "1.15", From 38fa4964f8eafb2cecabe5e3279642eada34d7d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Dec 2021 18:53:14 +0000 Subject: [PATCH 33/33] Bump detekt-gradle-plugin from 1.18.1 to 1.19.0 Bumps [detekt-gradle-plugin](https://github.com/detekt/detekt) from 1.18.1 to 1.19.0. - [Release notes](https://github.com/detekt/detekt/releases) - [Commits](https://github.com/detekt/detekt/compare/v1.18.1...v1.19.0) --- updated-dependencies: - dependency-name: io.gitlab.arturbosch.detekt:detekt-gradle-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 7903647b..e706d2fd 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -10,7 +10,7 @@ ext.versions = [ androidxcore : "1.6.0", ktlint : "0.37.1", ktlintGradle : "10.2.0", - detekt : "1.18.1", + detekt : "1.19.0", jacoco : "0.8.7", preferences : "1.1.1", media : "1.3.1",