mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-03-03 19:08:54 +01:00
Added coverArt images to Artists
Minor fixes
This commit is contained in:
parent
34a6413f10
commit
47e5675d1e
@ -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);
|
||||
|
@ -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.");
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) }
|
||||
|
@ -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 {
|
||||
|
@ -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()) }
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -5,6 +5,7 @@
|
||||
<corners
|
||||
android:topLeftRadius="44dp"
|
||||
android:topRightRadius="44dp"
|
||||
android:bottomRightRadius="44dp"
|
||||
android:bottomLeftRadius="44dp" />
|
||||
|
||||
<padding
|
||||
|
@ -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" />
|
||||
|
@ -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"/>
|
||||
|
Loading…
x
Reference in New Issue
Block a user