mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-13 07:57:16 +03:00
Move ImageLoader module into main module.
This commit is contained in:
parent
f8efb6d592
commit
9161f9dc99
@ -1,29 +0,0 @@
|
|||||||
apply from: bootstrap.androidModule
|
|
||||||
|
|
||||||
android {
|
|
||||||
buildFeatures {
|
|
||||||
buildConfig = true
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api project(':core:domain')
|
|
||||||
api project(':core:subsonic-api')
|
|
||||||
api(other.picasso) {
|
|
||||||
exclude group: "com.android.support"
|
|
||||||
}
|
|
||||||
|
|
||||||
testImplementation testing.kotlinJunit
|
|
||||||
testImplementation testing.mockito
|
|
||||||
testImplementation testing.mockitoInline
|
|
||||||
testImplementation testing.mockitoKotlin
|
|
||||||
testImplementation testing.kluent
|
|
||||||
testImplementation testing.robolectric
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="org.moire.ultrasonic.subsonic.loader.image">
|
|
||||||
</manifest>
|
|
@ -1,5 +1,4 @@
|
|||||||
include ':core:domain'
|
include ':core:domain'
|
||||||
include ':core:subsonic-api'
|
include ':core:subsonic-api'
|
||||||
include ':core:subsonic-api-image-loader'
|
|
||||||
include ':core:cache'
|
include ':core:cache'
|
||||||
include ':ultrasonic'
|
include ':ultrasonic'
|
||||||
|
@ -70,9 +70,12 @@ tasks.withType(Test) {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':core:domain')
|
implementation project(':core:domain')
|
||||||
implementation project(':core:subsonic-api')
|
implementation project(':core:subsonic-api')
|
||||||
implementation project(':core:subsonic-api-image-loader')
|
|
||||||
implementation project(':core:cache')
|
implementation project(':core:cache')
|
||||||
|
|
||||||
|
api(other.picasso) {
|
||||||
|
exclude group: "com.android.support"
|
||||||
|
}
|
||||||
|
|
||||||
implementation androidSupport.core
|
implementation androidSupport.core
|
||||||
implementation androidSupport.support
|
implementation androidSupport.support
|
||||||
implementation androidSupport.design
|
implementation androidSupport.design
|
||||||
@ -103,8 +106,12 @@ dependencies {
|
|||||||
testImplementation testing.junit
|
testImplementation testing.junit
|
||||||
testRuntimeOnly testing.junitVintage
|
testRuntimeOnly testing.junitVintage
|
||||||
testImplementation testing.kotlinJunit
|
testImplementation testing.kotlinJunit
|
||||||
testImplementation testing.mockitoKotlin
|
|
||||||
testImplementation testing.kluent
|
testImplementation testing.kluent
|
||||||
|
testImplementation testing.mockito
|
||||||
|
testImplementation testing.mockitoInline
|
||||||
|
testImplementation testing.mockitoKotlin
|
||||||
|
testImplementation testing.robolectric
|
||||||
|
|
||||||
implementation other.dexter
|
implementation other.dexter
|
||||||
implementation other.timber
|
implementation other.timber
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ public class NowPlayingFragment extends Fragment {
|
|||||||
String title = song.getTitle();
|
String title = song.getTitle();
|
||||||
String artist = song.getArtist();
|
String artist = song.getArtist();
|
||||||
|
|
||||||
imageLoader.getValue().getImageLoader().loadImage(nowPlayingAlbumArtImage, song, false, Util.getNotificationImageSize(getContext()), false, true);
|
imageLoader.getValue().getImageLoader().loadImage(nowPlayingAlbumArtImage, song, false, Util.getNotificationImageSize(getContext()));
|
||||||
nowPlayingTrack.setText(title);
|
nowPlayingTrack.setText(title);
|
||||||
nowPlayingArtist.setText(artist);
|
nowPlayingArtist.setText(artist);
|
||||||
|
|
||||||
|
@ -1306,7 +1306,7 @@ public class PlayerFragment extends Fragment implements GestureDetector.OnGestur
|
|||||||
artistTextView.setText(currentSong.getArtist());
|
artistTextView.setText(currentSong.getArtist());
|
||||||
downloadTrackTextView.setText(trackFormat);
|
downloadTrackTextView.setText(trackFormat);
|
||||||
downloadTotalDurationTextView.setText(duration);
|
downloadTotalDurationTextView.setText(duration);
|
||||||
imageLoaderProvider.getValue().getImageLoader().loadImage(albumArtImageView, currentSong, true, 0, false, true);
|
imageLoaderProvider.getValue().getImageLoader().loadImage(albumArtImageView, currentSong, true, 0);
|
||||||
|
|
||||||
displaySongRating();
|
displaySongRating();
|
||||||
}
|
}
|
||||||
@ -1318,7 +1318,7 @@ public class PlayerFragment extends Fragment implements GestureDetector.OnGestur
|
|||||||
artistTextView.setText(null);
|
artistTextView.setText(null);
|
||||||
downloadTrackTextView.setText(null);
|
downloadTrackTextView.setText(null);
|
||||||
downloadTotalDurationTextView.setText(null);
|
downloadTotalDurationTextView.setText(null);
|
||||||
imageLoaderProvider.getValue().getImageLoader().loadImage(albumArtImageView, null, true, 0, false, true);
|
imageLoaderProvider.getValue().getImageLoader().loadImage(albumArtImageView, null, true, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider;
|
|||||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
import org.moire.ultrasonic.domain.MusicDirectory;
|
||||||
import org.moire.ultrasonic.service.MusicService;
|
import org.moire.ultrasonic.service.MusicService;
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory;
|
import org.moire.ultrasonic.service.MusicServiceFactory;
|
||||||
import org.moire.ultrasonic.util.ImageLoader;
|
import org.moire.ultrasonic.imageloader.ImageLoader;
|
||||||
import org.moire.ultrasonic.util.Util;
|
import org.moire.ultrasonic.util.Util;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,7 +109,7 @@ public class AlbumView extends UpdateView
|
|||||||
public void setAlbum(final MusicDirectory.Entry album)
|
public void setAlbum(final MusicDirectory.Entry album)
|
||||||
{
|
{
|
||||||
viewHolder.cover_art.setTag(album);
|
viewHolder.cover_art.setTag(album);
|
||||||
imageLoader.loadImage(viewHolder.cover_art, album, false, 0, false, true);
|
imageLoader.loadImage(viewHolder.cover_art, album, false, 0);
|
||||||
this.entry = album;
|
this.entry = album;
|
||||||
|
|
||||||
String title = album.getTitle();
|
String title = album.getTitle();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.moire.ultrasonic.view;
|
package org.moire.ultrasonic.view;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.text.util.Linkify;
|
import android.text.util.Linkify;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@ -12,8 +13,8 @@ import android.widget.TextView;
|
|||||||
import org.moire.ultrasonic.R;
|
import org.moire.ultrasonic.R;
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||||
import org.moire.ultrasonic.domain.ChatMessage;
|
import org.moire.ultrasonic.domain.ChatMessage;
|
||||||
|
import org.moire.ultrasonic.imageloader.ImageLoader;
|
||||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
|
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
|
||||||
import org.moire.ultrasonic.util.ImageLoader;
|
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -33,7 +34,7 @@ public class ChatAdapter extends ArrayAdapter<ChatMessage>
|
|||||||
private static final Pattern phoneMatcher = Pattern.compile(phoneRegex);
|
private static final Pattern phoneMatcher = Pattern.compile(phoneRegex);
|
||||||
|
|
||||||
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
|
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
|
||||||
private final Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
|
private final Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
|
||||||
|
|
||||||
public ChatAdapter(Context context, List<ChatMessage> messages)
|
public ChatAdapter(Context context, List<ChatMessage> messages)
|
||||||
{
|
{
|
||||||
@ -95,11 +96,11 @@ public class ChatAdapter extends ArrayAdapter<ChatMessage>
|
|||||||
DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(context);
|
DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(context);
|
||||||
String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime));
|
String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime));
|
||||||
|
|
||||||
ImageLoader imageLoaderInstance = imageLoader.getValue().getImageLoader();
|
ImageLoader imageLoader = imageLoaderProvider.getValue().getImageLoader();
|
||||||
|
|
||||||
if (imageLoaderInstance != null)
|
if (holder.avatar != null && !TextUtils.isEmpty(messageUser))
|
||||||
{
|
{
|
||||||
imageLoaderInstance.loadAvatarImage(holder.avatar, messageUser, false, holder.avatar.getWidth(), false, true);
|
imageLoader.loadAvatarImage(holder.avatar, messageUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.username.setText(messageUser);
|
holder.username.setText(messageUser);
|
||||||
|
@ -28,7 +28,7 @@ import android.widget.LinearLayout;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
|
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
|
||||||
import org.moire.ultrasonic.util.ImageLoader;
|
import org.moire.ultrasonic.imageloader.ImageLoader;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ import org.moire.ultrasonic.subsonic.DownloadHandler
|
|||||||
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
||||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
import org.moire.ultrasonic.subsonic.ShareHandler
|
||||||
import org.moire.ultrasonic.subsonic.VideoPlayer
|
import org.moire.ultrasonic.subsonic.VideoPlayer
|
||||||
import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader
|
import org.moire.ultrasonic.imageloader.SubsonicImageLoader
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,8 +77,6 @@ val musicServiceModule = module {
|
|||||||
OfflineMusicService()
|
OfflineMusicService()
|
||||||
}
|
}
|
||||||
|
|
||||||
single { SubsonicImageLoader(androidContext(), get()) }
|
|
||||||
|
|
||||||
single { DownloadHandler(get(), get()) }
|
single { DownloadHandler(get(), get()) }
|
||||||
single { NetworkAndStorageChecker(androidContext()) }
|
single { NetworkAndStorageChecker(androidContext()) }
|
||||||
single { VideoPlayer() }
|
single { VideoPlayer() }
|
||||||
|
@ -15,7 +15,7 @@ import android.widget.TextView
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.util.ImageLoader
|
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Row in a RecyclerView which contains the details of an Album
|
* Creates a Row in a RecyclerView which contains the details of an Album
|
||||||
@ -57,7 +57,7 @@ class AlbumRowAdapter(
|
|||||||
imageLoader.loadImage(
|
imageLoader.loadImage(
|
||||||
holder.coverArt,
|
holder.coverArt,
|
||||||
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
|
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
|
||||||
false, 0, false, true, R.drawable.unknown_album
|
false, 0, R.drawable.unknown_album
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import java.text.Collator
|
|||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.util.ImageLoader
|
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,7 +62,7 @@ class ArtistRowAdapter(
|
|||||||
imageLoader.loadImage(
|
imageLoader.loadImage(
|
||||||
holder.coverArt,
|
holder.coverArt,
|
||||||
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
|
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
|
||||||
false, 0, false, true, R.drawable.ic_contact_picture
|
false, 0, R.drawable.ic_contact_picture
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
holder.coverArt.visibility = View.GONE
|
holder.coverArt.visibility = View.GONE
|
||||||
|
@ -763,7 +763,7 @@ class TrackCollectionFragment : Fragment() {
|
|||||||
val artworkSelection = random.nextInt(entries.size)
|
val artworkSelection = random.nextInt(entries.size)
|
||||||
imageLoaderProvider.getImageLoader().loadImage(
|
imageLoaderProvider.getImageLoader().loadImage(
|
||||||
coverArtView, entries[artworkSelection], false,
|
coverArtView, entries[artworkSelection], false,
|
||||||
Util.getAlbumImageSize(context), false, true
|
Util.getAlbumImageSize(context)
|
||||||
)
|
)
|
||||||
|
|
||||||
val albumHeader = AlbumHeader.processEntries(context, entries)
|
val albumHeader = AlbumHeader.processEntries(context, entries)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package org.moire.ultrasonic.subsonic.loader.image
|
package org.moire.ultrasonic.imageloader
|
||||||
|
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import com.squareup.picasso.Request
|
import com.squareup.picasso.Request
|
||||||
@ -15,9 +15,7 @@ class AvatarRequestHandler(
|
|||||||
) : RequestHandler() {
|
) : RequestHandler() {
|
||||||
override fun canHandleRequest(data: Request): Boolean {
|
override fun canHandleRequest(data: Request): Boolean {
|
||||||
return with(data.uri) {
|
return with(data.uri) {
|
||||||
scheme == SCHEME &&
|
scheme == SCHEME && path == "/$AVATAR_PATH"
|
||||||
authority == AUTHORITY &&
|
|
||||||
path == "/$AVATAR_PATH"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package org.moire.ultrasonic.subsonic.loader.image
|
package org.moire.ultrasonic.imageloader
|
||||||
|
|
||||||
import com.squareup.picasso.Picasso.LoadedFrom.NETWORK
|
import com.squareup.picasso.Picasso.LoadedFrom.NETWORK
|
||||||
import com.squareup.picasso.Request
|
import com.squareup.picasso.Request
|
||||||
@ -14,7 +14,6 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request
|
|||||||
override fun canHandleRequest(data: Request): Boolean {
|
override fun canHandleRequest(data: Request): Boolean {
|
||||||
return with(data.uri) {
|
return with(data.uri) {
|
||||||
scheme == SCHEME &&
|
scheme == SCHEME &&
|
||||||
authority == AUTHORITY &&
|
|
||||||
path == "/$COVER_ART_PATH"
|
path == "/$COVER_ART_PATH"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,157 @@
|
|||||||
|
package org.moire.ultrasonic.imageloader
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import com.squareup.picasso.RequestCreator
|
||||||
|
import org.moire.ultrasonic.BuildConfig
|
||||||
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||||
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Our new image loader which uses Picasso as a backend.
|
||||||
|
*/
|
||||||
|
class ImageLoader(
|
||||||
|
context: Context,
|
||||||
|
apiClient: SubsonicAPIClient,
|
||||||
|
private val config: ImageLoaderConfig
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val picasso = Picasso.Builder(context)
|
||||||
|
.addRequestHandler(CoverArtRequestHandler(apiClient))
|
||||||
|
.addRequestHandler(AvatarRequestHandler(apiClient))
|
||||||
|
.build().apply {
|
||||||
|
setIndicatorsEnabled(BuildConfig.DEBUG)
|
||||||
|
Picasso.setSingletonInstance(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load(request: ImageRequest) = when (request) {
|
||||||
|
is ImageRequest.CoverArt -> loadCoverArt(request)
|
||||||
|
is ImageRequest.Avatar -> loadAvatar(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadCoverArt(request: ImageRequest.CoverArt) {
|
||||||
|
picasso.load(createLoadCoverArtRequest(request.entityId, request.size.toLong()))
|
||||||
|
.addPlaceholder(request)
|
||||||
|
.addError(request)
|
||||||
|
.stableKey("${request.entityId}-${request.size}")
|
||||||
|
.into(request.imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadAvatar(request: ImageRequest.Avatar) {
|
||||||
|
picasso.load(createLoadAvatarRequest(request.username))
|
||||||
|
.addPlaceholder(request)
|
||||||
|
.addError(request)
|
||||||
|
.stableKey(request.username)
|
||||||
|
.into(request.imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RequestCreator.addPlaceholder(request: ImageRequest): RequestCreator {
|
||||||
|
if (request.placeHolderDrawableRes != null) {
|
||||||
|
placeholder(request.placeHolderDrawableRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RequestCreator.addError(request: ImageRequest): RequestCreator {
|
||||||
|
if (request.errorDrawableRes != null) {
|
||||||
|
error(request.errorDrawableRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the cover of a given entry into an ImageView
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
fun loadImage(
|
||||||
|
view: View?,
|
||||||
|
entry: MusicDirectory.Entry?,
|
||||||
|
large: Boolean,
|
||||||
|
size: Int,
|
||||||
|
defaultResourceId: Int = R.drawable.unknown_album
|
||||||
|
) {
|
||||||
|
val id = entry?.coverArt
|
||||||
|
val requestedSize = resolveSize(size, large)
|
||||||
|
|
||||||
|
if (id != null && id.isNotEmpty() && view is ImageView) {
|
||||||
|
val request = ImageRequest.CoverArt(
|
||||||
|
id, view, requestedSize,
|
||||||
|
placeHolderDrawableRes = defaultResourceId,
|
||||||
|
errorDrawableRes = defaultResourceId
|
||||||
|
)
|
||||||
|
load(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the avatar of a given user into an ImageView
|
||||||
|
*/
|
||||||
|
fun loadAvatarImage(
|
||||||
|
view: ImageView,
|
||||||
|
username: String
|
||||||
|
) {
|
||||||
|
if (username.isNotEmpty()) {
|
||||||
|
val request = ImageRequest.Avatar(
|
||||||
|
username, view,
|
||||||
|
placeHolderDrawableRes = R.drawable.ic_contact_picture,
|
||||||
|
errorDrawableRes = R.drawable.ic_contact_picture
|
||||||
|
)
|
||||||
|
load(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveSize(requested: Int, large: Boolean): Int {
|
||||||
|
if (requested <= 0) {
|
||||||
|
return if (large) config.largeSize else config.defaultSize
|
||||||
|
} else {
|
||||||
|
return requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data classes to hold all the info we need later on to process the request
|
||||||
|
*/
|
||||||
|
sealed class ImageRequest(
|
||||||
|
val placeHolderDrawableRes: Int? = null,
|
||||||
|
val errorDrawableRes: Int? = null,
|
||||||
|
val imageView: ImageView
|
||||||
|
) {
|
||||||
|
class CoverArt(
|
||||||
|
val entityId: String,
|
||||||
|
imageView: ImageView,
|
||||||
|
val size: Int,
|
||||||
|
placeHolderDrawableRes: Int? = null,
|
||||||
|
errorDrawableRes: Int? = null,
|
||||||
|
) : ImageRequest(
|
||||||
|
placeHolderDrawableRes,
|
||||||
|
errorDrawableRes,
|
||||||
|
imageView
|
||||||
|
)
|
||||||
|
|
||||||
|
class Avatar(
|
||||||
|
val username: String,
|
||||||
|
imageView: ImageView,
|
||||||
|
placeHolderDrawableRes: Int? = null,
|
||||||
|
errorDrawableRes: Int? = null
|
||||||
|
) : ImageRequest(
|
||||||
|
placeHolderDrawableRes,
|
||||||
|
errorDrawableRes,
|
||||||
|
imageView
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to configure an instance of the ImageLoader
|
||||||
|
*/
|
||||||
|
data class ImageLoaderConfig (
|
||||||
|
val largeSize: Int = 0,
|
||||||
|
val defaultSize: Int = 0,
|
||||||
|
val cacheFolder: File?
|
||||||
|
)
|
@ -1,9 +1,8 @@
|
|||||||
package org.moire.ultrasonic.subsonic.loader.image
|
package org.moire.ultrasonic.imageloader
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
|
||||||
internal const val SCHEME = "subsonic_api"
|
internal const val SCHEME = "subsonic_api"
|
||||||
internal const val AUTHORITY = BuildConfig.LIBRARY_PACKAGE_NAME
|
|
||||||
internal const val COVER_ART_PATH = "cover_art"
|
internal const val COVER_ART_PATH = "cover_art"
|
||||||
internal const val AVATAR_PATH = "avatar"
|
internal const val AVATAR_PATH = "avatar"
|
||||||
internal const val QUERY_ID = "id"
|
internal const val QUERY_ID = "id"
|
||||||
@ -17,7 +16,6 @@ internal const val QUERY_USERNAME = "username"
|
|||||||
internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri =
|
internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri =
|
||||||
Uri.Builder()
|
Uri.Builder()
|
||||||
.scheme(SCHEME)
|
.scheme(SCHEME)
|
||||||
.authority(AUTHORITY)
|
|
||||||
.appendPath(COVER_ART_PATH)
|
.appendPath(COVER_ART_PATH)
|
||||||
.appendQueryParameter(QUERY_ID, entityId)
|
.appendQueryParameter(QUERY_ID, entityId)
|
||||||
.appendQueryParameter(SIZE, size.toString())
|
.appendQueryParameter(SIZE, size.toString())
|
||||||
@ -26,7 +24,6 @@ internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri =
|
|||||||
internal fun createLoadAvatarRequest(username: String): Uri =
|
internal fun createLoadAvatarRequest(username: String): Uri =
|
||||||
Uri.Builder()
|
Uri.Builder()
|
||||||
.scheme(SCHEME)
|
.scheme(SCHEME)
|
||||||
.authority(AUTHORITY)
|
|
||||||
.appendPath(AVATAR_PATH)
|
.appendPath(AVATAR_PATH)
|
||||||
.appendQueryParameter(QUERY_USERNAME, username)
|
.appendQueryParameter(QUERY_USERNAME, username)
|
||||||
.build()
|
.build()
|
@ -1,12 +1,11 @@
|
|||||||
package org.moire.ultrasonic.subsonic.loader.image
|
package org.moire.ultrasonic.imageloader
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.ImageView
|
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import com.squareup.picasso.RequestCreator
|
import com.squareup.picasso.RequestCreator
|
||||||
|
import org.moire.ultrasonic.BuildConfig
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||||
|
|
||||||
// TODO: Implement OkHTTP disk caching
|
|
||||||
class SubsonicImageLoader(
|
class SubsonicImageLoader(
|
||||||
context: Context,
|
context: Context,
|
||||||
apiClient: SubsonicAPIClient
|
apiClient: SubsonicAPIClient
|
||||||
@ -56,32 +55,3 @@ class SubsonicImageLoader(
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class ImageRequest(
|
|
||||||
val placeHolderDrawableRes: Int? = null,
|
|
||||||
val errorDrawableRes: Int? = null,
|
|
||||||
val imageView: ImageView
|
|
||||||
) {
|
|
||||||
class CoverArt(
|
|
||||||
val entityId: String,
|
|
||||||
imageView: ImageView,
|
|
||||||
val size: Int,
|
|
||||||
placeHolderDrawableRes: Int? = null,
|
|
||||||
errorDrawableRes: Int? = null,
|
|
||||||
) : ImageRequest(
|
|
||||||
placeHolderDrawableRes,
|
|
||||||
errorDrawableRes,
|
|
||||||
imageView
|
|
||||||
)
|
|
||||||
|
|
||||||
class Avatar(
|
|
||||||
val username: String,
|
|
||||||
imageView: ImageView,
|
|
||||||
placeHolderDrawableRes: Int? = null,
|
|
||||||
errorDrawableRes: Int? = null
|
|
||||||
) : ImageRequest(
|
|
||||||
placeHolderDrawableRes,
|
|
||||||
errorDrawableRes,
|
|
||||||
imageView
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,9 +1,15 @@
|
|||||||
package org.moire.ultrasonic.subsonic
|
package org.moire.ultrasonic.subsonic
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import org.moire.ultrasonic.util.ImageLoader
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.app.UApp
|
||||||
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
|
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||||
|
import org.moire.ultrasonic.imageloader.ImageLoaderConfig
|
||||||
|
import org.moire.ultrasonic.util.Util
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the lifetime of the Image Loader
|
* Handles the lifetime of the Image Loader
|
||||||
@ -11,6 +17,26 @@ import org.moire.ultrasonic.util.ImageLoader
|
|||||||
class ImageLoaderProvider(val context: Context) : KoinComponent {
|
class ImageLoaderProvider(val context: Context) : KoinComponent {
|
||||||
private var imageLoader: ImageLoader? = null
|
private var imageLoader: ImageLoader? = null
|
||||||
|
|
||||||
|
private val config by lazy {
|
||||||
|
var defaultSize = 0
|
||||||
|
val fallbackImage = ResourcesCompat.getDrawable(
|
||||||
|
UApp.applicationContext().resources, R.drawable.unknown_album, null
|
||||||
|
)
|
||||||
|
|
||||||
|
// Determine the density-dependent image sizes by taking the fallback album
|
||||||
|
// image and querying its size.
|
||||||
|
if (fallbackImage != null) {
|
||||||
|
defaultSize = fallbackImage.intrinsicHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageLoaderConfig(
|
||||||
|
Util.getMaxDisplayMetric(),
|
||||||
|
defaultSize,
|
||||||
|
FileUtil.getAlbumArtDirectory()
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun clearImageLoader() {
|
fun clearImageLoader() {
|
||||||
imageLoader = null
|
imageLoader = null
|
||||||
@ -19,7 +45,7 @@ class ImageLoaderProvider(val context: Context) : KoinComponent {
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
fun getImageLoader(): ImageLoader {
|
fun getImageLoader(): ImageLoader {
|
||||||
if (imageLoader == null) {
|
if (imageLoader == null) {
|
||||||
imageLoader = SubsonicImageLoaderProxy(get())
|
imageLoader = ImageLoader(get(), get(), config)
|
||||||
}
|
}
|
||||||
return imageLoader!!
|
return imageLoader!!
|
||||||
}
|
}
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
package org.moire.ultrasonic.subsonic
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import org.moire.ultrasonic.R
|
|
||||||
import org.moire.ultrasonic.app.UApp
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
|
||||||
import org.moire.ultrasonic.subsonic.loader.image.ImageRequest
|
|
||||||
import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader
|
|
||||||
import org.moire.ultrasonic.util.ImageLoader
|
|
||||||
import org.moire.ultrasonic.util.Util
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Proxy between [SubsonicImageLoader] and the main App.
|
|
||||||
* Needed to calculate values like the maximum image size,
|
|
||||||
* which we can't outside the main package.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class SubsonicImageLoaderProxy(
|
|
||||||
private val subsonicImageLoader: SubsonicImageLoader
|
|
||||||
) : ImageLoader {
|
|
||||||
|
|
||||||
private var imageSizeLarge = Util.getMaxDisplayMetric()
|
|
||||||
private var imageSizeDefault = 0
|
|
||||||
|
|
||||||
override fun loadImage(
|
|
||||||
view: View?,
|
|
||||||
entry: MusicDirectory.Entry?,
|
|
||||||
large: Boolean,
|
|
||||||
size: Int,
|
|
||||||
crossFade: Boolean,
|
|
||||||
highQuality: Boolean
|
|
||||||
) {
|
|
||||||
return loadImage(view, entry, large, size, crossFade, highQuality, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadImage(
|
|
||||||
view: View?,
|
|
||||||
entry: MusicDirectory.Entry?,
|
|
||||||
large: Boolean,
|
|
||||||
size: Int,
|
|
||||||
crossFade: Boolean,
|
|
||||||
highQuality: Boolean,
|
|
||||||
defaultResourceId: Int
|
|
||||||
) {
|
|
||||||
val id = entry?.coverArt
|
|
||||||
var requestedSize = size
|
|
||||||
val unknownImageId =
|
|
||||||
if (defaultResourceId == -1) R.drawable.unknown_album
|
|
||||||
else defaultResourceId
|
|
||||||
|
|
||||||
if (requestedSize <= 0) {
|
|
||||||
requestedSize = if (large) imageSizeLarge else imageSizeDefault
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id != null && id.isNotEmpty() && view is ImageView) {
|
|
||||||
val request = ImageRequest.CoverArt(
|
|
||||||
id, view, requestedSize,
|
|
||||||
placeHolderDrawableRes = unknownImageId,
|
|
||||||
errorDrawableRes = unknownImageId
|
|
||||||
)
|
|
||||||
subsonicImageLoader.load(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadAvatarImage(
|
|
||||||
view: View?,
|
|
||||||
username: String?,
|
|
||||||
large: Boolean,
|
|
||||||
size: Int,
|
|
||||||
crossFade: Boolean,
|
|
||||||
highQuality: Boolean
|
|
||||||
) {
|
|
||||||
if (username != null && username.isNotEmpty() && view is ImageView) {
|
|
||||||
val request = ImageRequest.Avatar(
|
|
||||||
username, view,
|
|
||||||
placeHolderDrawableRes = R.drawable.ic_contact_picture,
|
|
||||||
errorDrawableRes = R.drawable.ic_contact_picture
|
|
||||||
)
|
|
||||||
subsonicImageLoader.load(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
val default = ResourcesCompat.getDrawable(
|
|
||||||
UApp.applicationContext().resources, R.drawable.unknown_album, null
|
|
||||||
)
|
|
||||||
|
|
||||||
// Determine the density-dependent image sizes by taking the fallback album
|
|
||||||
// image and querying its size.
|
|
||||||
if (default != null) {
|
|
||||||
imageSizeDefault = default.intrinsicHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
package org.moire.ultrasonic.subsonic.loader.image
|
package org.moire.ultrasonic.imageloader
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.nhaarman.mockito_kotlin.any
|
import com.nhaarman.mockito_kotlin.any
|
||||||
@ -21,7 +21,8 @@ import org.robolectric.annotation.Config
|
|||||||
@Config(manifest = Config.NONE)
|
@Config(manifest = Config.NONE)
|
||||||
class AvatarRequestHandlerTest {
|
class AvatarRequestHandlerTest {
|
||||||
private val mockSubsonicApiClient = mock<SubsonicAPIClient>()
|
private val mockSubsonicApiClient = mock<SubsonicAPIClient>()
|
||||||
private val handler = AvatarRequestHandler(mockSubsonicApiClient)
|
private val handler =
|
||||||
|
AvatarRequestHandler(mockSubsonicApiClient)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should accept only cover art request`() {
|
fun `Should accept only cover art request`() {
|
||||||
@ -34,7 +35,6 @@ class AvatarRequestHandlerTest {
|
|||||||
fun `Should not accept random request uri`() {
|
fun `Should not accept random request uri`() {
|
||||||
val requestUri = Uri.Builder()
|
val requestUri = Uri.Builder()
|
||||||
.scheme(SCHEME)
|
.scheme(SCHEME)
|
||||||
.authority(AUTHORITY)
|
|
||||||
.appendPath("something")
|
.appendPath("something")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@ -63,7 +63,8 @@ class AvatarRequestHandlerTest {
|
|||||||
whenever(mockSubsonicApiClient.getAvatar(any()))
|
whenever(mockSubsonicApiClient.getAvatar(any()))
|
||||||
.thenReturn(streamResponse)
|
.thenReturn(streamResponse)
|
||||||
|
|
||||||
val response = handler.load(createLoadAvatarRequest("some-username").buildRequest(), 0)
|
val response = handler.load(
|
||||||
|
createLoadAvatarRequest("some-username").buildRequest(), 0)
|
||||||
|
|
||||||
response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK
|
response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK
|
||||||
response.source `should not be` null
|
response.source `should not be` null
|
@ -1,4 +1,4 @@
|
|||||||
package org.moire.ultrasonic.subsonic.loader.image
|
package org.moire.ultrasonic.imageloader
|
||||||
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import okio.Okio
|
import okio.Okio
|
@ -1,4 +1,4 @@
|
|||||||
package org.moire.ultrasonic.subsonic.loader.image
|
package org.moire.ultrasonic.imageloader
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.nhaarman.mockito_kotlin.any
|
import com.nhaarman.mockito_kotlin.any
|
||||||
@ -21,7 +21,8 @@ import org.robolectric.RobolectricTestRunner
|
|||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class CoverArtRequestHandlerTest {
|
class CoverArtRequestHandlerTest {
|
||||||
private val mockSubsonicApiClientMock = mock<SubsonicAPIClient>()
|
private val mockSubsonicApiClientMock = mock<SubsonicAPIClient>()
|
||||||
private val handler = CoverArtRequestHandler(mockSubsonicApiClientMock)
|
private val handler =
|
||||||
|
CoverArtRequestHandler(mockSubsonicApiClientMock)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should accept only cover art request`() {
|
fun `Should accept only cover art request`() {
|
||||||
@ -34,7 +35,6 @@ class CoverArtRequestHandlerTest {
|
|||||||
fun `Should not accept random request uri`() {
|
fun `Should not accept random request uri`() {
|
||||||
val requestUri = Uri.Builder()
|
val requestUri = Uri.Builder()
|
||||||
.scheme(SCHEME)
|
.scheme(SCHEME)
|
||||||
.authority(AUTHORITY)
|
|
||||||
.appendPath("random")
|
.appendPath("random")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@ -76,7 +76,8 @@ class CoverArtRequestHandlerTest {
|
|||||||
whenever(mockSubsonicApiClientMock.getCoverArt(any(), anyOrNull()))
|
whenever(mockSubsonicApiClientMock.getCoverArt(any(), anyOrNull()))
|
||||||
.thenReturn(streamResponse)
|
.thenReturn(streamResponse)
|
||||||
|
|
||||||
val response = handler.load(createLoadCoverArtRequest("some").buildRequest(), 0)
|
val response = handler.load(
|
||||||
|
createLoadCoverArtRequest("some").buildRequest(), 0)
|
||||||
|
|
||||||
response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK
|
response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK
|
||||||
response.source `should not be` null
|
response.source `should not be` null
|
@ -1,4 +1,4 @@
|
|||||||
package org.moire.ultrasonic.subsonic.loader.image
|
package org.moire.ultrasonic.imageloader
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
@ -13,7 +13,7 @@ class RequestCreatorTest {
|
|||||||
val entityId = "299"
|
val entityId = "299"
|
||||||
val size = 100L
|
val size = 100L
|
||||||
val expectedUri =
|
val expectedUri =
|
||||||
Uri.parse("$SCHEME://$AUTHORITY/$COVER_ART_PATH?$QUERY_ID=$entityId&$SIZE=$size")
|
Uri.parse("$SCHEME:/$COVER_ART_PATH?$QUERY_ID=$entityId&$SIZE=$size")
|
||||||
|
|
||||||
createLoadCoverArtRequest(entityId, size).compareTo(expectedUri).shouldBeEqualTo(0)
|
createLoadCoverArtRequest(entityId, size).compareTo(expectedUri).shouldBeEqualTo(0)
|
||||||
}
|
}
|
||||||
@ -21,7 +21,7 @@ class RequestCreatorTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `Should create valid avatar request`() {
|
fun `Should create valid avatar request`() {
|
||||||
val username = "some-username"
|
val username = "some-username"
|
||||||
val expectedUri = Uri.parse("$SCHEME://$AUTHORITY/$AVATAR_PATH?$QUERY_USERNAME=$username")
|
val expectedUri = Uri.parse("$SCHEME:/$AVATAR_PATH?$QUERY_USERNAME=$username")
|
||||||
|
|
||||||
createLoadAvatarRequest(username).compareTo(expectedUri).shouldBeEqualTo(0)
|
createLoadAvatarRequest(username).compareTo(expectedUri).shouldBeEqualTo(0)
|
||||||
}
|
}
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Loading…
x
Reference in New Issue
Block a user