Refactor coil
Completely refactor coil and how its used so that it centers around data objects instead of a hodgepodge of URIs and Song data.
This commit is contained in:
parent
54f9ceca90
commit
e0485ebad9
23 changed files with 211 additions and 461 deletions
81
app/src/main/java/org/oxycblt/auxio/coil/AlbumArtFetcher.kt
Normal file
81
app/src/main/java/org/oxycblt/auxio/coil/AlbumArtFetcher.kt
Normal file
|
@ -0,0 +1,81 @@
|
|||
package org.oxycblt.auxio.coil
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaMetadataRetriever
|
||||
import coil.bitmap.BitmapPool
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.Options
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.size.Size
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.toURI
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Fetcher that returns the album art for a given [Album]. Handles settings on whether to use
|
||||
* quality covers or not.
|
||||
*/
|
||||
class AlbumArtFetcher(private val context: Context) : Fetcher<Album> {
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
override suspend fun fetch(
|
||||
pool: BitmapPool,
|
||||
data: Album,
|
||||
size: Size,
|
||||
options: Options
|
||||
): FetchResult {
|
||||
return if (settingsManager.useQualityCovers) {
|
||||
loadQualityCovers(data)
|
||||
} else {
|
||||
loadMediaStoreCovers(data)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMediaStoreCovers(data: Album): SourceResult {
|
||||
val stream: InputStream? = context.contentResolver.openInputStream(data.coverUri)
|
||||
|
||||
stream?.let { stm ->
|
||||
return SourceResult(
|
||||
source = stm.source().buffer(),
|
||||
mimeType = context.contentResolver.getType(data.coverUri),
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
error("No cover art for album ${data.name}")
|
||||
}
|
||||
|
||||
private fun loadQualityCovers(data: Album): SourceResult {
|
||||
val extractor = MediaMetadataRetriever()
|
||||
|
||||
extractor.use { ext ->
|
||||
val songUri = data.songs[0].id.toURI()
|
||||
ext.setDataSource(context, songUri)
|
||||
|
||||
// Get the embedded picture from MediaMetadataRetriever, which will return a full
|
||||
// ByteArray of the cover without any compression artifacts.
|
||||
// If its null [a.k.a there is no embedded cover], than just ignore it and move on
|
||||
ext.embeddedPicture?.let { coverBytes ->
|
||||
val stream = ByteArrayInputStream(coverBytes)
|
||||
|
||||
return SourceResult(
|
||||
source = stream.source().buffer(),
|
||||
mimeType = context.contentResolver.getType(songUri),
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// If we are here, the extractor likely failed so instead attempt to return the compressed
|
||||
// cover instead.
|
||||
return loadMediaStoreCovers(data)
|
||||
}
|
||||
|
||||
override fun key(data: Album) = data.id.toString()
|
||||
}
|
|
@ -2,7 +2,6 @@ package org.oxycblt.auxio.coil
|
|||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.widget.ImageView
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.databinding.BindingAdapter
|
||||
|
@ -15,185 +14,111 @@ import org.oxycblt.auxio.music.Genre
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
|
||||
private val ignoreCovers: Boolean get() = !SettingsManager.getInstance().showCovers
|
||||
|
||||
/**
|
||||
* Get a bitmap for a song. onDone will be called when the bitmap is loaded.
|
||||
* **Do not use this on the UI elements, instead use the Binding Adapters.**
|
||||
* **This not meant for UIs, instead use the Binding Adapters.**
|
||||
* @param context [Context] required
|
||||
* @param song Song to load the cover for
|
||||
* @param onDone What to do with the bitmap when the loading is finished. Bitmap will be null if loading failed/shouldn't occur.
|
||||
*/
|
||||
fun getBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) {
|
||||
if (!SettingsManager.getInstance().showCovers) {
|
||||
fun loadBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) {
|
||||
if (ignoreCovers) {
|
||||
onDone(null)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val request = ImageRequest.Builder(context)
|
||||
.doCoverSetup(context, song)
|
||||
.target(onError = { onDone(null) }, onSuccess = { onDone(it.toBitmap()) })
|
||||
.build()
|
||||
|
||||
Coil.imageLoader(context).enqueue(request)
|
||||
Coil.imageLoader(context).enqueue(
|
||||
ImageRequest.Builder(context)
|
||||
.data(song.album)
|
||||
.fetcher(AlbumArtFetcher(context))
|
||||
.target(
|
||||
onError = { onDone(null) },
|
||||
onSuccess = { onDone(it.toBitmap()) }
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
// --- BINDING ADAPTERS ---
|
||||
|
||||
/**
|
||||
* Bind the cover art for a song.
|
||||
* Bind the album art for a [Song].
|
||||
*/
|
||||
@BindingAdapter("coverArt")
|
||||
fun ImageView.bindCoverArt(song: Song) {
|
||||
if (!SettingsManager.getInstance().showCovers) {
|
||||
@BindingAdapter("albumArt")
|
||||
fun ImageView.bindAlbumArt(song: Song) {
|
||||
if (ignoreCovers) {
|
||||
setImageResource(R.drawable.ic_song)
|
||||
return
|
||||
}
|
||||
|
||||
val request = newRequest()
|
||||
.doCoverSetup(context, song)
|
||||
.error(R.drawable.ic_song)
|
||||
.build()
|
||||
|
||||
Coil.imageLoader(context).enqueue(request)
|
||||
Coil.imageLoader(context).enqueue(
|
||||
ImageRequest.Builder(context)
|
||||
.target(this)
|
||||
.data(song.album)
|
||||
.fetcher(AlbumArtFetcher(context))
|
||||
.error(R.drawable.ic_song)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the cover art for an album
|
||||
* Bind the album art for an [Album].
|
||||
*/
|
||||
@BindingAdapter("coverArt")
|
||||
fun ImageView.bindCoverArt(album: Album) {
|
||||
if (!SettingsManager.getInstance().showCovers) {
|
||||
@BindingAdapter("albumArt")
|
||||
fun ImageView.bindAlbumArt(album: Album) {
|
||||
if (ignoreCovers) {
|
||||
setImageResource(R.drawable.ic_album)
|
||||
return
|
||||
}
|
||||
|
||||
val request = newRequest()
|
||||
.doCoverSetup(context, album)
|
||||
.error(R.drawable.ic_album)
|
||||
.build()
|
||||
|
||||
Coil.imageLoader(context).enqueue(request)
|
||||
Coil.imageLoader(context).enqueue(
|
||||
ImageRequest.Builder(context)
|
||||
.target(this)
|
||||
.data(album)
|
||||
.fetcher(AlbumArtFetcher(context))
|
||||
.error(R.drawable.ic_album)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the artist image for an artist.
|
||||
* Bind the image for an [Artist]
|
||||
*/
|
||||
@BindingAdapter("artistImage")
|
||||
fun ImageView.bindArtistImage(artist: Artist) {
|
||||
if (!SettingsManager.getInstance().showCovers) {
|
||||
if (ignoreCovers) {
|
||||
setImageResource(R.drawable.ic_artist)
|
||||
return
|
||||
}
|
||||
|
||||
val request: ImageRequest
|
||||
|
||||
// If there is more than one album, then create a mosaic of them.
|
||||
if (artist.albums.size >= 4) {
|
||||
val uris = mutableListOf<Uri>()
|
||||
|
||||
for (i in 0..3) {
|
||||
uris.add(artist.albums[i].coverUri)
|
||||
}
|
||||
|
||||
val fetcher = MosaicFetcher(context)
|
||||
|
||||
request = newRequest()
|
||||
.data(uris)
|
||||
.fetcher(fetcher)
|
||||
Coil.imageLoader(context).enqueue(
|
||||
ImageRequest.Builder(context)
|
||||
.target(this)
|
||||
.data(artist)
|
||||
.fetcher(MosaicFetcher(context))
|
||||
.error(R.drawable.ic_artist)
|
||||
.build()
|
||||
} else {
|
||||
// Otherwise, just get the first cover and use that
|
||||
// If the artist doesn't have any albums [Which happens], then don't even bother with that.
|
||||
if (artist.albums.isNotEmpty()) {
|
||||
request = newRequest()
|
||||
.doCoverSetup(context, artist.albums[0])
|
||||
.error(R.drawable.ic_artist)
|
||||
.build()
|
||||
} else {
|
||||
setImageResource(R.drawable.ic_artist)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Coil.imageLoader(context).enqueue(request)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the genre image for a genre.
|
||||
* Bind the image for a [Genre]
|
||||
*/
|
||||
@BindingAdapter("genreImage")
|
||||
fun ImageView.bindGenreImage(genre: Genre) {
|
||||
if (!SettingsManager.getInstance().showCovers) {
|
||||
if (ignoreCovers) {
|
||||
setImageResource(R.drawable.ic_genre)
|
||||
return
|
||||
}
|
||||
|
||||
val request: ImageRequest
|
||||
val genreCovers = mutableListOf<Uri>()
|
||||
|
||||
// Group the genre's songs by their album's cover and add them
|
||||
genre.songs.groupBy { it.album.coverUri }.forEach { genreCovers.add(it.key) }
|
||||
|
||||
if (genreCovers.size >= 4) {
|
||||
val fetcher = MosaicFetcher(context)
|
||||
|
||||
request = newRequest()
|
||||
.data(genreCovers.slice(0..3))
|
||||
.fetcher(fetcher)
|
||||
Coil.imageLoader(context).enqueue(
|
||||
ImageRequest.Builder(context)
|
||||
.target(this)
|
||||
.data(genre)
|
||||
.fetcher(MosaicFetcher(context))
|
||||
.error(R.drawable.ic_genre)
|
||||
.build()
|
||||
} else {
|
||||
if (genreCovers.isNotEmpty()) {
|
||||
request = newRequest()
|
||||
.doCoverSetup(context, genre.songs[0])
|
||||
.error(R.drawable.ic_genre)
|
||||
.build()
|
||||
} else {
|
||||
setImageResource(R.drawable.ic_genre)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Coil.imageLoader(context).enqueue(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a high quality or low-quality cover needs to be loaded for a specific [Album]
|
||||
* @return The same builder that this is applied to
|
||||
*/
|
||||
private fun ImageRequest.Builder.doCoverSetup(context: Context, data: Album): ImageRequest.Builder {
|
||||
if (SettingsManager.getInstance().useQualityCovers) {
|
||||
fetcher(QualityCoverFetcher(context))
|
||||
data(data.songs[0])
|
||||
} else {
|
||||
data(data.coverUri)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a high quality or low-quality cover needs to be loaded for a specific [Song]
|
||||
* @return The same builder that this is applied to
|
||||
*/
|
||||
private fun ImageRequest.Builder.doCoverSetup(context: Context, data: Song): ImageRequest.Builder {
|
||||
if (SettingsManager.getInstance().useQualityCovers) {
|
||||
fetcher(QualityCoverFetcher(context))
|
||||
data(data)
|
||||
} else {
|
||||
data(data.album.coverUri)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base request used by the above functions
|
||||
* @return The base request
|
||||
*/
|
||||
private fun ImageView.newRequest(): ImageRequest.Builder {
|
||||
return ImageRequest.Builder(context).target(this)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,24 +16,43 @@ import coil.fetch.SourceResult
|
|||
import coil.size.Size
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Parent
|
||||
import java.io.Closeable
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* A [Fetcher] that takes multiple cover uris and turns them into a 2x2 mosaic image.
|
||||
* A [Fetcher] that takes an [Artist] or [Genre] and returns a mosaic of its albums.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class MosaicFetcher(private val context: Context) : Fetcher<List<Uri>> {
|
||||
class MosaicFetcher(private val context: Context) : Fetcher<Parent> {
|
||||
override suspend fun fetch(
|
||||
pool: BitmapPool,
|
||||
data: List<Uri>,
|
||||
data: Parent,
|
||||
size: Size,
|
||||
options: Options
|
||||
): FetchResult {
|
||||
// Get the URIs for either a genre or artist
|
||||
val uris = mutableListOf<Uri>()
|
||||
|
||||
when (data) {
|
||||
is Artist -> data.albums.forEachIndexed { index, album ->
|
||||
if (index < 4) { uris.add(album.coverUri) }
|
||||
}
|
||||
|
||||
is Genre -> data.songs.groupBy { it.album.coverUri }.keys.forEachIndexed { index, uri ->
|
||||
if (index < 4) { uris.add(uri) }
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
val streams = mutableListOf<InputStream>()
|
||||
|
||||
// Load MediaStore streams
|
||||
data.forEach {
|
||||
uris.forEach {
|
||||
val stream: InputStream? = context.contentResolver.openInputStream(it)
|
||||
|
||||
if (stream != null) {
|
||||
|
@ -44,26 +63,35 @@ class MosaicFetcher(private val context: Context) : Fetcher<List<Uri>> {
|
|||
// If so many streams failed that there's not enough images to make a mosaic, then
|
||||
// just return the first cover image.
|
||||
if (streams.size < 4) {
|
||||
streams.forEach { it.close() }
|
||||
// Dont even bother if ALL the streams have failed.
|
||||
check(streams.isNotEmpty()) { "All streams have failed. " }
|
||||
|
||||
return if (streams.isNotEmpty()) {
|
||||
SourceResult(
|
||||
source = streams[0].source().buffer(),
|
||||
mimeType = context.contentResolver.getType(data[0]),
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
} else {
|
||||
error("All streams failed. Not bothering.")
|
||||
}
|
||||
return SourceResult(
|
||||
source = streams[0].source().buffer(),
|
||||
mimeType = context.contentResolver.getType(uris[0]),
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
// Create the mosaic, code adapted from Phonograph.
|
||||
// https://github.com/kabouzeid/Phonograph
|
||||
val finalBitmap = Bitmap.createBitmap(
|
||||
val bitmap = drawMosaic(streams)
|
||||
|
||||
return DrawableResult(
|
||||
drawable = bitmap.toDrawable(context.resources),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the mosaic, Code adapted from Phonograph
|
||||
* https://github.com/kabouzeid/Phonograph
|
||||
*/
|
||||
private fun drawMosaic(streams: List<InputStream>): Bitmap {
|
||||
val mosaicBitmap = Bitmap.createBitmap(
|
||||
MOSAIC_BITMAP_SIZE, MOSAIC_BITMAP_SIZE, Bitmap.Config.RGB_565
|
||||
)
|
||||
|
||||
val canvas = Canvas(finalBitmap)
|
||||
val canvas = Canvas(mosaicBitmap)
|
||||
|
||||
var x = 0
|
||||
var y = 0
|
||||
|
@ -90,11 +118,7 @@ class MosaicFetcher(private val context: Context) : Fetcher<List<Uri>> {
|
|||
}
|
||||
}
|
||||
|
||||
return DrawableResult(
|
||||
drawable = finalBitmap.toDrawable(context.resources),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
return mosaicBitmap
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,7 +131,8 @@ class MosaicFetcher(private val context: Context) : Fetcher<List<Uri>> {
|
|||
}
|
||||
}
|
||||
|
||||
override fun key(data: List<Uri>): String = data.toString()
|
||||
override fun key(data: Parent): String = data.id.toString()
|
||||
override fun handles(data: Parent) = data !is Album // Albums are not used here
|
||||
|
||||
companion object {
|
||||
private const val MOSAIC_BITMAP_SIZE = 512
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
package org.oxycblt.auxio.coil
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import coil.bitmap.BitmapPool
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.Options
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.size.Size
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.toURI
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* A [Fetcher] for fetching high-quality embedded covers instead of the compressed covers, albeit
|
||||
* at the cost of load time & memory usage.
|
||||
*/
|
||||
class QualityCoverFetcher(private val context: Context) : Fetcher<Song> {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
override suspend fun fetch(
|
||||
pool: BitmapPool,
|
||||
data: Song,
|
||||
size: Size,
|
||||
options: Options
|
||||
): FetchResult {
|
||||
val extractor = MediaMetadataRetriever()
|
||||
val stream: InputStream?
|
||||
val uri: Uri
|
||||
|
||||
extractor.use { ext ->
|
||||
ext.setDataSource(context, data.id.toURI())
|
||||
val cover = ext.embeddedPicture
|
||||
|
||||
stream = if (cover != null) {
|
||||
uri = data.id.toURI()
|
||||
|
||||
ByteArrayInputStream(cover)
|
||||
} else {
|
||||
// Fallback to the compressed cover if the cover loading failed.
|
||||
uri = data.album.coverUri
|
||||
|
||||
// Blocking call, but coil is on a background thread so it doesn't matter
|
||||
context.contentResolver.openInputStream(data.album.coverUri)
|
||||
}
|
||||
|
||||
stream?.use {
|
||||
return SourceResult(
|
||||
source = it.source().buffer(),
|
||||
mimeType = context.contentResolver.getType(uri),
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// If we are here, the extractor likely failed so instead attempt to return the compressed
|
||||
// cover instead.
|
||||
context.contentResolver.openInputStream(data.album.coverUri)?.use {
|
||||
return SourceResult(
|
||||
source = it.source().buffer(),
|
||||
mimeType = context.contentResolver.getType(uri),
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
// If even that failed, then error out entirely.
|
||||
error("Likely no bitmap for this song/album.")
|
||||
}
|
||||
|
||||
// Group bitmaps by their album so that caching is more efficent
|
||||
override fun key(data: Song): String = data.album.id.toString()
|
||||
}
|
|
@ -13,7 +13,7 @@ import androidx.media.app.NotificationCompat.MediaStyle
|
|||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.MainActivity
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.coil.getBitmap
|
||||
import org.oxycblt.auxio.coil.loadBitmap
|
||||
import org.oxycblt.auxio.logE
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
@ -110,7 +110,7 @@ fun NotificationCompat.Builder.setMetadata(
|
|||
if (colorize) {
|
||||
// getBitmap() is concurrent, so only call back to the object calling this function when
|
||||
// the loading is over.
|
||||
getBitmap(context, song) {
|
||||
loadBitmap(context, song) {
|
||||
setLargeIcon(it)
|
||||
|
||||
onDone()
|
||||
|
|
|
@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.conflate
|
|||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.coil.getBitmap
|
||||
import org.oxycblt.auxio.coil.loadBitmap
|
||||
import org.oxycblt.auxio.logD
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.toURI
|
||||
|
@ -392,7 +392,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
|||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album.name)
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration)
|
||||
|
||||
getBitmap(this, song) {
|
||||
loadBitmap(this, song) {
|
||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, it)
|
||||
mediaSession.setMetadata(builder.build())
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:layout_marginBottom="@dimen/margin_small"
|
||||
android:contentDescription="@{@string/description_album_cover(song.name)}"
|
||||
app:coverArt="@{song}"
|
||||
app:albumArt="@{song}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
android:contentDescription="@{@string/description_album_cover(song.name)}"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
android:outlineProvider="bounds"
|
||||
app:coverArt="@{song}"
|
||||
app:albumArt="@{song}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
android:contentDescription="@{@string/description_album_cover(album.name)}"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
android:outlineProvider="bounds"
|
||||
app:coverArt="@{album}"
|
||||
app:albumArt="@{album}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_album" />
|
||||
|
|
|
@ -1,208 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".playback.PlaybackFragment">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="song"
|
||||
type="org.oxycblt.auxio.music.Song" />
|
||||
|
||||
<variable
|
||||
name="playbackModel"
|
||||
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
|
||||
|
||||
<variable
|
||||
name="detailModel"
|
||||
type="org.oxycblt.auxio.detail.DetailViewModel" />
|
||||
</data>
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/playback_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/playback_toolbar"
|
||||
style="@style/Toolbar.Style.Icon"
|
||||
android:elevation="0dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:menu="@menu/menu_playback"
|
||||
app:navigationIcon="@drawable/ic_down"
|
||||
app:title="@string/label_playback" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/playback_cover"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="@dimen/margin_huge"
|
||||
android:contentDescription="@{@string/description_album_cover(song.name)}"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
android:outlineProvider="bounds"
|
||||
app:coverArt="@{song}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/playback_song"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar"
|
||||
tools:src="@drawable/ic_song" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_song"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_huge"
|
||||
android:layout_marginEnd="@dimen/margin_huge"
|
||||
android:ellipsize="marquee"
|
||||
android:fontFamily="@font/inter_semibold"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:onClick="@{() -> detailModel.navToItem(playbackModel.song)}"
|
||||
android:singleLine="true"
|
||||
android:text="@{song.name}"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintBottom_toTopOf="@+id/playback_artist"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Song Name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_artist"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_huge"
|
||||
android:layout_marginEnd="@dimen/margin_huge"
|
||||
android:ellipsize="end"
|
||||
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
|
||||
android:singleLine="true"
|
||||
android:text="@{song.album.artist.name}"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/playback_album"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="Artist Name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_album"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_huge"
|
||||
android:layout_marginEnd="@dimen/margin_huge"
|
||||
android:layout_marginBottom="@dimen/margin_medium"
|
||||
android:ellipsize="end"
|
||||
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album)}"
|
||||
android:singleLine="true"
|
||||
android:text="@{song.album.name}"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="Album Name" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/playback_seek_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:paddingStart="@dimen/margin_huge"
|
||||
android:paddingEnd="@dimen/margin_huge"
|
||||
android:progressBackgroundTint="?android:attr/colorControlNormal"
|
||||
android:progressTint="?attr/colorPrimary"
|
||||
android:splitTrack="false"
|
||||
android:thumbOffset="@dimen/offset_thumb"
|
||||
android:thumbTint="?attr/colorPrimary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/playback_duration_current"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:progress="70" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_duration_current"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_huge"
|
||||
android:layout_marginBottom="@dimen/margin_medium"
|
||||
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="11:38" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_song_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/margin_huge"
|
||||
android:layout_marginBottom="@dimen/margin_medium"
|
||||
android:text="@{song.formattedDuration}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:text="16:16" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/playback_loop"
|
||||
style="@style/Widget.Button.Unbounded"
|
||||
android:layout_marginEnd="@dimen/margin_large"
|
||||
android:contentDescription="@string/description_change_loop"
|
||||
android:onClick="@{() -> playbackModel.incrementLoopStatus()}"
|
||||
android:src="@drawable/ic_loop"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_prev"
|
||||
app:layout_constraintEnd_toStartOf="@+id/playback_skip_prev"
|
||||
app:layout_constraintTop_toTopOf="@+id/playback_skip_prev" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/playback_skip_prev"
|
||||
style="@style/Widget.Button.Unbounded"
|
||||
android:layout_marginEnd="@dimen/margin_large"
|
||||
android:contentDescription="@string/description_skip_prev"
|
||||
android:onClick="@{() -> playbackModel.skipPrev()}"
|
||||
android:src="@drawable/ic_skip_prev"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
||||
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
|
||||
app:layout_constraintTop_toTopOf="@+id/playback_play_pause" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/playback_play_pause"
|
||||
style="@style/PlayPause"
|
||||
android:layout_marginBottom="@dimen/margin_large"
|
||||
android:contentDescription="@string/description_play_pause"
|
||||
android:onClick="@{() -> playbackModel.invertPlayingStatus()}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/playback_song_duration"
|
||||
app:layout_constraintStart_toStartOf="@+id/playback_duration_current"
|
||||
tools:src="@drawable/ic_play_to_pause" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/playback_skip_next"
|
||||
style="@style/Widget.Button.Unbounded"
|
||||
android:layout_marginStart="@dimen/margin_large"
|
||||
android:contentDescription="@string/description_skip_next"
|
||||
android:onClick="@{() -> playbackModel.skipNext()}"
|
||||
android:src="@drawable/ic_skip_next"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
||||
app:layout_constraintStart_toEndOf="@+id/playback_play_pause"
|
||||
app:layout_constraintTop_toTopOf="@+id/playback_play_pause" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/playback_shuffle"
|
||||
style="@style/Widget.Button.Unbounded"
|
||||
android:layout_marginStart="@dimen/margin_large"
|
||||
android:contentDescription="@{playbackModel.isShuffling() ? @string/description_shuffle_off : @string/description_shuffle_on"
|
||||
android:onClick="@{() -> playbackModel.invertShuffleStatus()}"
|
||||
android:src="@drawable/ic_shuffle"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_next"
|
||||
app:layout_constraintStart_toEndOf="@+id/playback_skip_next"
|
||||
app:layout_constraintTop_toTopOf="@+id/playback_skip_next" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
|
@ -32,7 +32,7 @@
|
|||
android:contentDescription="@{@string/description_album_cover(album.name)}"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
android:outlineProvider="bounds"
|
||||
app:coverArt="@{album}"
|
||||
app:albumArt="@{album}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_album" />
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
android:contentDescription="@{@string/description_album_cover(song.name)}"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
android:outlineProvider="bounds"
|
||||
app:coverArt="@{song}"
|
||||
app:albumArt="@{song}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
|
@ -42,7 +42,7 @@
|
|||
android:layout_height="@dimen/size_cover_compact"
|
||||
android:layout_margin="@dimen/margin_mid_small"
|
||||
android:contentDescription="@{@string/description_album_cover(song.name)}"
|
||||
app:coverArt="@{song}"
|
||||
app:albumArt="@{song}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/playback_progress"
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
android:contentDescription="@{@string/description_album_cover(song.name)}"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
android:outlineProvider="bounds"
|
||||
app:coverArt="@{song}"
|
||||
app:albumArt="@{song}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/playback_song"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
android:layout_width="@dimen/size_cover_normal"
|
||||
android:layout_height="@dimen/size_cover_normal"
|
||||
android:contentDescription="@{@string/description_album_cover(album.name)}"
|
||||
app:coverArt="@{album}"
|
||||
app:albumArt="@{album}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
android:contentDescription="@{@string/description_album_cover(album.name)}"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
android:outlineProvider="bounds"
|
||||
app:coverArt="@{album}"
|
||||
app:albumArt="@{album}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
android:layout_width="@dimen/size_cover_large"
|
||||
android:layout_height="@dimen/size_cover_large"
|
||||
android:contentDescription="@{@string/description_album_cover(album.name)}"
|
||||
app:coverArt="@{album}"
|
||||
app:albumArt="@{album}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
android:layout_width="@dimen/size_cover_compact"
|
||||
android:layout_height="@dimen/size_cover_compact"
|
||||
android:contentDescription="@{@string/description_album_cover(song.name)}"
|
||||
app:coverArt="@{song}"
|
||||
app:albumArt="@{song}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
android:layout_width="@dimen/size_cover_compact"
|
||||
android:layout_height="@dimen/size_cover_compact"
|
||||
android:contentDescription="@{@string/description_album_cover(song.name)}"
|
||||
app:coverArt="@{song}"
|
||||
app:albumArt="@{song}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
android:layout_width="@dimen/size_cover_compact"
|
||||
android:layout_height="@dimen/size_cover_compact"
|
||||
android:contentDescription="@{@string/description_album_cover(song.name)}"
|
||||
app:coverArt="@{song}"
|
||||
app:albumArt="@{song}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<dimen name="margin_medium">16dp</dimen>
|
||||
<dimen name="margin_mid_large">24dp</dimen>
|
||||
<dimen name="margin_large">32dp</dimen>
|
||||
<dimen name="margin_huge">48dp</dimen>
|
||||
<dimen name="margin_mid_huge">48dp</dimen>
|
||||
<dimen name="margin_insane">128dp</dimen>
|
||||
|
||||
<!-- Height Namespace | Height for UI elements -->
|
||||
|
|
|
@ -8,7 +8,7 @@ These will likely be accepted as long as they do not cause too much harm to the
|
|||
|
||||
## New Customizations/Options
|
||||
|
||||
While I do like adding new behavior/UI customizations, these will be looked at more closely as certain additions can cause harm to the apps UI/UX while not providing alot of benefit. These tend to be accpeted however.
|
||||
While I do like adding new behavior/UI customizations, these will be looked at more closely as certain additions can cause harm to the apps UI/UX while not providing alot of benefit. These tend to be accepted however.
|
||||
|
||||
## Feature Addtions and UI Changes
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ org.oxycblt.auxio # Main UI's and logging utilities
|
|||
- `app:genreImage`: Binding Adapter that will load the genre image
|
||||
- `getBitmap`: Function that will take a song and return a bitmap, this should not be used in anything UI related, that is what the binding adapters above are for.
|
||||
|
||||
This should be enough to cover most use cases in Auxio. There are also fetchers for artist/genre images and higher quality covers, but these are not used outside of the module.
|
||||
This should be enough to cover most use cases in Auxio. There are also fetchers for artist/genre images and album covers, but these are not used outside of the module.
|
||||
|
||||
#### `.database`
|
||||
|
||||
|
@ -117,6 +117,10 @@ PlaybackStateManager───────────────────┘
|
|||
|
||||
Shared RecyclerView utilities, often for adapters and ViewHolders. Important ones to note are `DiffCallback`, which acts as a reusable differ callback based off of `BaseModel` for `ListAdapter`s, and the shared ViewHolders for each data type, such as `SongViewHolder` or `HeaderViewHolder`.
|
||||
|
||||
#### `.settings`
|
||||
|
||||
The settings system is primarily based off of `SettingsManager`, a wrapper around `SharedPreferences`. This allows settings to be read/written in a much simpler/safer manner and without a context being needed. The Settings UI is largely contained in `SettingsListFragment`, while the `.ui` sub-package contains UIs related to the settings UI, such as the About Dialog.
|
||||
|
||||
#### `.search`
|
||||
|
||||
Package for Auxio's search functionality, `SearchViewHolder` handles the data results and filtering while `SearchFragment`/`SearchAdapter` handles the display of the results and user input.
|
||||
|
@ -129,5 +133,5 @@ Package for the songs UI, there is no data management here, only a user interfac
|
|||
|
||||
Shared User Interface utilities. This is primarily made up of convenience/extension functions relating to Views, Resources, Configurations, and Contexts. It also contains some dedicated utilities, such as:
|
||||
- The Accent Management system
|
||||
- `newMenu` and `ActionMenu`, which automates menu creation for most datatypes
|
||||
- `memberBinding` and `MemberBinder`, which allows for viewbindings to be used as a member variable without memory leaks or nullability issues.
|
||||
- `newMenu` and `ActionMenu`, which automates menu creation for most data types
|
||||
- `memberBinding` and `MemberBinder`, which allows for ViewBindings to be used as a member variable without memory leaks or nullability issues.
|
Loading…
Reference in a new issue