From 47e5675d1e3c9f3a5fd31bb951e7064a797238b3 Mon Sep 17 00:00:00 2001 From: Nite Date: Wed, 18 Nov 2020 21:46:46 +0100 Subject: [PATCH] Added coverArt images to Artists Minor fixes --- .../moire/ultrasonic/util/ImageLoader.java | 2 ++ .../ultrasonic/util/LegacyImageLoader.java | 36 +++++++++---------- .../ultrasonic/activity/ArtistListModel.kt | 17 +++++++-- .../ultrasonic/activity/ArtistRowAdapter.kt | 36 ++++++++++++++++++- .../activity/SelectArtistActivity.kt | 8 +++-- .../moire/ultrasonic/di/MusicServiceModule.kt | 4 ++- .../ultrasonic/domain/APIArtistConverter.kt | 1 + .../service/CommunicationErrorHandler.kt | 4 +++ ultrasonic/src/main/res/drawable/thumb.xml | 1 + .../src/main/res/layout/artist_list_item.xml | 28 +++++++++++---- ultrasonic/src/main/res/values/styles.xml | 5 +++ 11 files changed, 110 insertions(+), 32 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java index 74202148..9624301e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java @@ -31,6 +31,8 @@ public interface ImageLoader { boolean highQuality ); + void cancel(String coverArt); + Bitmap getImageBitmap(String username, int size); Bitmap getImageBitmap(MusicDirectory.Entry entry, boolean large, int size); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java index 3684c6b8..8da92508 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java @@ -38,6 +38,8 @@ import org.moire.ultrasonic.service.MusicServiceFactory; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; +import java.util.SortedSet; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; @@ -203,6 +205,16 @@ public class LegacyImageLoader implements Runnable, ImageLoader { queue.offer(new Task(view, entry, size, large, crossFade, highQuality)); } + public void cancel(String coverArt) { + for (Object taskObject : queue.toArray()) { + Task task = (Task)taskObject; + if ((task.entry.getCoverArt() != null) && (coverArt.compareTo(task.entry.getCoverArt()) == 0)) { + queue.remove(taskObject); + break; + } + } + } + private static String getKey(String coverArtId, int size) { return String.format("%s:%d", coverArtId, size); } @@ -381,14 +393,7 @@ public class LegacyImageLoader implements Runnable, ImageLoader { private final boolean crossFade; private final boolean highQuality; - Task( - View view, - MusicDirectory.Entry entry, - int size, - boolean saveToFile, - boolean crossFade, - boolean highQuality - ) { + Task(View view, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean crossFade, boolean highQuality) { this.view = view; this.entry = entry; this.username = null; @@ -399,14 +404,7 @@ public class LegacyImageLoader implements Runnable, ImageLoader { handler = new Handler(); } - Task( - View view, - String username, - int size, - boolean saveToFile, - boolean crossFade, - boolean highQuality - ) { + Task(View view, String username, int size, boolean saveToFile, boolean crossFade, boolean highQuality) { this.view = view; this.entry = null; this.username = username; @@ -421,9 +419,9 @@ public class LegacyImageLoader implements Runnable, ImageLoader { try { MusicService musicService = MusicServiceFactory.getMusicService(view.getContext()); final boolean isAvatar = this.username != null && this.entry == null; - final Bitmap bitmap = this.entry != null - ? musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) - : musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null); + final Bitmap bitmap = this.entry != null ? + musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) : + musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null); if (bitmap == null) { Timber.d("Found empty album art."); diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistListModel.kt index a8b7afdb..a12e6a43 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistListModel.kt @@ -36,6 +36,9 @@ import org.moire.ultrasonic.service.CommunicationErrorHandler import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.Util +/** + * Provides ViewModel which contains the list of available Artists + */ class ArtistListModel( private val activeServerProvider: ActiveServerProvider, private val context: Context @@ -43,23 +46,31 @@ class ArtistListModel( private val musicFolders: MutableLiveData> = MutableLiveData() private val artists: MutableLiveData> = MutableLiveData() + /** + * Retrieves the available Artists in a LiveData + */ fun getArtists(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> { backgroundLoadFromServer(refresh, swipe) return artists } - fun getMusicFolders(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> { - backgroundLoadFromServer(refresh, swipe) + /** + * Retrieves the available Music Folders in a LiveData + */ + fun getMusicFolders(): LiveData> { return musicFolders } + /** + * Refreshes the cached Artists from the server + */ fun refresh(swipe: SwipeRefreshLayout) { backgroundLoadFromServer(true, swipe) } private fun backgroundLoadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) { - swipe.isRefreshing = true viewModelScope.launch { + swipe.isRefreshing = true loadFromServer(refresh, swipe) swipe.isRefreshing = false } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistRowAdapter.kt index c187f2ab..4f355ba0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistRowAdapter.kt @@ -23,6 +23,7 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.widget.ImageView import android.widget.LinearLayout import android.widget.PopupMenu import android.widget.RelativeLayout @@ -31,34 +32,54 @@ import androidx.recyclerview.widget.RecyclerView import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.util.ImageLoader +/** + * Creates a Row in a RecyclerView which contains the details of an Artist + */ class ArtistRowAdapter( private var artistList: List, private var folderName: String, private var shouldShowHeader: Boolean, val onArtistClick: (Artist) -> Unit, val onContextMenuClick: (MenuItem, Artist) -> Boolean, - val onFolderClick: (view: View) -> Unit + val onFolderClick: (view: View) -> Unit, + val imageLoader: ImageLoader ) : RecyclerView.Adapter() { + /** + * Sets the data to be displayed in the RecyclerView + */ fun setData(data: List) { artistList = data.sortedBy { t -> t.name } notifyDataSetChanged() } + /** + * Sets the name of the folder to be displayed n the Header (first) row + */ fun setFolderName(name: String) { folderName = name notifyDataSetChanged() } + /** + * Holds the view properties of an Artist row + */ class ArtistViewHolder( 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 } + /** + * Holds the view properties of the Header row + */ class HeaderViewHolder( itemView: View ) : RecyclerView.ViewHolder(itemView) { @@ -80,6 +101,13 @@ class ArtistRowAdapter( return HeaderViewHolder(header) } + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + if ((holder is ArtistViewHolder) && (holder.coverArtId != null)) { + imageLoader.cancel(holder.coverArtId) + } + super.onViewRecycled(holder) + } + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { if (holder is ArtistViewHolder) { val listPosition = if (shouldShowHeader) position - 1 else position @@ -87,6 +115,12 @@ class ArtistRowAdapter( holder.section.text = getSectionForArtist(listPosition) holder.layout.setOnClickListener { onArtistClick(artistList[listPosition]) } holder.layout.setOnLongClickListener { view -> createPopupMenu(view, listPosition) } + holder.coverArtId = artistList[listPosition].coverArt + imageLoader.loadImage( + holder.coverArt, + MusicDirectory.Entry().apply { coverArt = holder.coverArtId }, + false, 0, false, true + ) } else if (holder is HeaderViewHolder) { holder.folderName.text = folderName holder.layout.setOnClickListener { onFolderClick(holder.layout) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/SelectArtistActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/SelectArtistActivity.kt index f822ea3d..87a4c4c8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/SelectArtistActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/SelectArtistActivity.kt @@ -37,6 +37,9 @@ import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Util +/** + * Displays the available Artists in a list + */ class SelectArtistActivity : SubsonicTabActivity() { private val activeServerProvider: ActiveServerProvider by inject() private val serverSettingsModel: ServerSettingsModel by viewModel() @@ -77,7 +80,7 @@ class SelectArtistActivity : SubsonicTabActivity() { val refresh = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false) - artistListModel.getMusicFolders(refresh, refreshArtistListView!!) + artistListModel.getMusicFolders() .observe( this, Observer { changedFolders -> @@ -100,7 +103,8 @@ class SelectArtistActivity : SubsonicTabActivity() { shouldShowHeader, { artist -> onItemClick(artist) }, { menuItem, artist -> onArtistMenuItemSelected(menuItem, artist) }, - { view -> onFolderClick(view) } + { view -> onFolderClick(view) }, + imageLoader ) artistListView = findViewById(R.id.select_artist_list).apply { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index ae6b75c6..42ac9b6b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -4,6 +4,7 @@ package org.moire.ultrasonic.di import kotlin.math.abs import okhttp3.logging.HttpLoggingInterceptor import org.koin.android.ext.koin.androidContext +import org.koin.android.viewmodel.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module import org.moire.ultrasonic.BuildConfig @@ -72,5 +73,6 @@ val musicServiceModule = module { } single { SubsonicImageLoader(androidContext(), get()) } - single { ArtistListModel(get(), get()) } + + viewModel { ArtistListModel(get(), androidContext()) } } 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 d7381164..cec72778 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt @@ -7,6 +7,7 @@ import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist fun APIArtist.toDomainEntity(): Artist = Artist( id = this@toDomainEntity.id, + coverArt = this@toDomainEntity.coverArt, name = this@toDomainEntity.name ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt index 23436a7c..39a72dcf 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt @@ -32,6 +32,10 @@ import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage import org.moire.ultrasonic.util.Util import timber.log.Timber +/** + * Contains helper functions to handle the exceptions + * thrown during the communication with a Subsonic server + */ class CommunicationErrorHandler { companion object { fun handleError(error: Throwable?, context: Context) { diff --git a/ultrasonic/src/main/res/drawable/thumb.xml b/ultrasonic/src/main/res/drawable/thumb.xml index 1a5a8aad..078a12c1 100644 --- a/ultrasonic/src/main/res/drawable/thumb.xml +++ b/ultrasonic/src/main/res/drawable/thumb.xml @@ -5,6 +5,7 @@ + + diff --git a/ultrasonic/src/main/res/values/styles.xml b/ultrasonic/src/main/res/values/styles.xml index 9334649d..65012f6d 100644 --- a/ultrasonic/src/main/res/values/styles.xml +++ b/ultrasonic/src/main/res/values/styles.xml @@ -35,6 +35,11 @@ center_vertical + +