Added coverArt images to Artists

Minor fixes
This commit is contained in:
Nite 2020-11-18 21:46:46 +01:00
parent 34a6413f10
commit 47e5675d1e
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
11 changed files with 110 additions and 32 deletions

View File

@ -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);

View File

@ -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.");

View File

@ -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<List<MusicFolder>> = MutableLiveData()
private val artists: MutableLiveData<List<Artist>> = MutableLiveData()
/**
* Retrieves the available Artists in a LiveData
*/
fun getArtists(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<Artist>> {
backgroundLoadFromServer(refresh, swipe)
return artists
}
fun getMusicFolders(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<MusicFolder>> {
backgroundLoadFromServer(refresh, swipe)
/**
* Retrieves the available Music Folders in a LiveData
*/
fun getMusicFolders(): LiveData<List<MusicFolder>> {
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
}

View File

@ -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<Artist>,
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<RecyclerView.ViewHolder>() {
/**
* Sets the data to be displayed in the RecyclerView
*/
fun setData(data: List<Artist>) {
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) }

View File

@ -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<RecyclerView>(R.id.select_artist_list).apply {

View File

@ -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()) }
}

View File

@ -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
)

View File

@ -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) {

View File

@ -5,6 +5,7 @@
<corners
android:topLeftRadius="44dp"
android:topRightRadius="44dp"
android:bottomRightRadius="44dp"
android:bottomLeftRadius="44dp" />
<padding

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
a:id="@+id/row_artist_layout"
a:layout_height="wrap_content"
a:layout_width="match_parent"
@ -13,24 +14,39 @@
a:layout_height="wrap_content"
a:gravity="center_horizontal|center_vertical"
a:minWidth="72dip"
a:minHeight="50dip"
a:minHeight="56dip"
a:paddingStart="16dip"
a:paddingLeft="16dip"
a:paddingEnd="32dip"
a:paddingRight="32dip"
a:paddingEnd="16dip"
a:paddingRight="16dip"
a:text="A"
a:textAppearance="?android:attr/textAppearanceLarge"
a:textColor="@color/cyan" />
<com.google.android.material.imageview.ShapeableImageView
a:id="@+id/artist_coverart"
a:src="@drawable/unknown_album"
app:shapeAppearanceOverlay="@style/roundedImageView"
a:layout_width="40dp"
a:layout_height="40dp"
a:layout_toEndOf="@+id/row_section"
a:layout_toRightOf="@+id/row_section"
a:layout_gravity="center_horizontal|center_vertical"
a:layout_marginTop="8dp"
a:layout_marginLeft="8dp"
a:layout_marginRight="16dp"
a:layout_marginStart="8dp"
a:layout_marginEnd="16dp" />
<TextView
a:id="@+id/row_artist_name"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:layout_toEndOf="@+id/row_section"
a:layout_toRightOf="@+id/row_section"
a:layout_toEndOf="@+id/artist_coverart"
a:layout_toRightOf="@+id/artist_coverart"
a:drawablePadding="6dip"
a:gravity="center_vertical"
a:minHeight="50dip"
a:minHeight="56dip"
a:paddingLeft="3dip"
a:paddingRight="3dip"
a:textAppearance="?android:attr/textAppearanceMedium" />

View File

@ -35,6 +35,11 @@
<item name="android:gravity">center_vertical</item>
</style>
<style name="roundedImageView" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">8dp</item>
</style>
<attr name="star_hollow" format="reference"/>
<attr name="star_full" format="reference"/>
<attr name="about" format="reference"/>