coil: completely refactor image loading
Upgrade to coil 2.0.0 and completely refactor the usage of coil to work with the new library structure. This also fixes the issue where error icons will just re-appear due to blocking calls. I had to add a fix on my end and also use the new caching system in coil 2.0.0.
This commit is contained in:
parent
e3f4a6fefa
commit
0e3ffb973b
24 changed files with 535 additions and 444 deletions
|
@ -37,6 +37,7 @@ android {
|
|||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs += "-Xjvm-default=all"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
|
@ -97,7 +98,7 @@ dependencies {
|
|||
implementation "com.google.android.exoplayer:exoplayer-core:2.16.0"
|
||||
|
||||
// Image loading
|
||||
implementation 'io.coil-kt:coil:1.4.0'
|
||||
implementation 'io.coil-kt:coil:2.0.0-alpha03'
|
||||
|
||||
// Material
|
||||
implementation "com.google.android.material:material:1.5.0-beta01"
|
||||
|
|
|
@ -22,6 +22,11 @@ import android.app.Application
|
|||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.request.CachePolicy
|
||||
import org.oxycblt.auxio.coil.AlbumArtFetcher
|
||||
import org.oxycblt.auxio.coil.ArtistImageFetcher
|
||||
import org.oxycblt.auxio.coil.CrossfadeTransition
|
||||
import org.oxycblt.auxio.coil.GenreImageFetcher
|
||||
import org.oxycblt.auxio.coil.MusicKeyer
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
|
||||
@Suppress("UNUSED")
|
||||
|
@ -36,9 +41,15 @@ class AuxioApp : Application(), ImageLoaderFactory {
|
|||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(applicationContext)
|
||||
.components {
|
||||
add(AlbumArtFetcher.SongFactory())
|
||||
add(AlbumArtFetcher.AlbumFactory())
|
||||
add(ArtistImageFetcher.Factory())
|
||||
add(GenreImageFetcher.Factory())
|
||||
add(MusicKeyer())
|
||||
}
|
||||
.transitionFactory(CrossfadeTransition.Factory())
|
||||
.diskCachePolicy(CachePolicy.DISABLED) // Not downloading anything, so no disk-caching
|
||||
.crossfade(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,7 @@ class MainFragment : Fragment(), PlaybackBarLayout.ActionCallback {
|
|||
|
||||
snackbar.show()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,234 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Auxio Project
|
||||
* AlbumArtFetcher.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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.Song
|
||||
import org.oxycblt.auxio.music.toAlbumArtURI
|
||||
import org.oxycblt.auxio.music.toURI
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
/**
|
||||
* Fetcher that returns the album art for a given [Album]. Handles settings on whether to use
|
||||
* quality covers or not.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AlbumArtFetcher(private val context: Context) : Fetcher<Album> {
|
||||
override suspend fun fetch(
|
||||
pool: BitmapPool,
|
||||
data: Album,
|
||||
size: Size,
|
||||
options: Options
|
||||
): FetchResult {
|
||||
val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
if (!settingsManager.showCovers) {
|
||||
error("Covers are disabled")
|
||||
}
|
||||
|
||||
val result = if (settingsManager.useQualityCovers) {
|
||||
fetchQualityCovers(data.songs[0])
|
||||
} else {
|
||||
// If we're fetching plain MediaStore covers, optimize for speed and don't go through
|
||||
// the wild goose chase that we do for quality covers.
|
||||
fetchMediaStoreCovers(data)
|
||||
}
|
||||
|
||||
checkNotNull(result) {
|
||||
"No cover art was found for ${data.name}"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun fetchQualityCovers(song: Song): FetchResult? {
|
||||
// Loading quality covers basically means to parse the file metadata ourselves
|
||||
// and then extract the cover.
|
||||
|
||||
// First try MediaMetadataRetriever. We will always do this first, as it supports
|
||||
// a variety of formats, has multiple levels of fault tolerance, and is pretty fast
|
||||
// for a manual parser.
|
||||
// However, Samsung seems to cripple this class as to force people to use their ad-infested
|
||||
// music app which relies on proprietary OneUI extensions instead of AOSP. That means
|
||||
// we have to have another layer of redundancy to retain quality. Thanks samsung. Prick.
|
||||
val result = fetchAospMetadataCovers(song)
|
||||
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
//
|
||||
// // Our next fallback is to rely on ExoPlayer's largely half-baked and undocumented
|
||||
// // metadata system.
|
||||
// val exoResult = fetchExoplayerCover(song)
|
||||
//
|
||||
// if (exoResult != null) {
|
||||
// return exoResult
|
||||
// }
|
||||
|
||||
// If the previous two failed, we resort to MediaStore's covers despite it literally
|
||||
// going against the point of this setting. The previous two calls are just too unreliable
|
||||
// and we can't do any filesystem traversing due to scoped storage.
|
||||
val mediaStoreResult = fetchMediaStoreCovers(song.album)
|
||||
|
||||
if (mediaStoreResult != null) {
|
||||
return mediaStoreResult
|
||||
}
|
||||
|
||||
// There is no cover we could feasibly fetch. Give up.
|
||||
return null
|
||||
}
|
||||
|
||||
private fun fetchMediaStoreCovers(data: Album): FetchResult? {
|
||||
val uri = data.id.toAlbumArtURI()
|
||||
val stream = context.contentResolver.openInputStream(uri)
|
||||
|
||||
if (stream != null) {
|
||||
// Don't close the stream here as it will cause an error later from an attempted read.
|
||||
// This stream still seems to close itself at some point, so its fine.
|
||||
return SourceResult(
|
||||
source = stream.source().buffer(),
|
||||
mimeType = context.contentResolver.getType(uri),
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun fetchAospMetadataCovers(song: Song): FetchResult? {
|
||||
val extractor = MediaMetadataRetriever()
|
||||
|
||||
extractor.use { ext ->
|
||||
val songUri = song.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)
|
||||
|
||||
stream.use { stm ->
|
||||
return SourceResult(
|
||||
source = stm.source().buffer(),
|
||||
mimeType = null,
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Disabled until I can figure out how the hell I can get a blocking call to play along in
|
||||
// a suspend function. I doubt it's possible.
|
||||
// private fun fetchExoplayerCover(song: Song): FetchResult? {
|
||||
// val uri = song.id.toURI()
|
||||
//
|
||||
// val future = MetadataRetriever.retrieveMetadata(
|
||||
// context, MediaItem.fromUri(song.id.toURI())
|
||||
// )
|
||||
//
|
||||
// // Coil is async, we can just spin until the loading has ended
|
||||
// while (future.isDone) { /* no-op */ }
|
||||
//
|
||||
// val tracks = try {
|
||||
// future.get()
|
||||
// } catch (e: Exception) {
|
||||
// null
|
||||
// }
|
||||
//
|
||||
// if (tracks == null || tracks.isEmpty) {
|
||||
// // Unrecognized format. This is expected, as ExoPlayer only supports a
|
||||
// // subset of formats.
|
||||
// return null
|
||||
// }
|
||||
//
|
||||
// // The metadata extraction process of ExoPlayer is normalized into a superclass.
|
||||
// // That means we have to iterate through and find the cover art ourselves.
|
||||
// val metadata = tracks[0].getFormat(0).metadata
|
||||
//
|
||||
// if (metadata == null || metadata.length() == 0) {
|
||||
// // No (parsable) metadata. This is also expected.
|
||||
// return null
|
||||
// }
|
||||
//
|
||||
// var stream: ByteArrayInputStream? = null
|
||||
//
|
||||
// for (i in 0 until metadata.length()) {
|
||||
// // We can only extract pictures from two tags with this method, ID3v2's APIC or
|
||||
// // FLAC's PICTURE.
|
||||
// val pic: ByteArray?
|
||||
// val type: Int
|
||||
//
|
||||
// when (val entry = metadata.get(i)) {
|
||||
// is ApicFrame -> {
|
||||
// pic = entry.pictureData
|
||||
// type = entry.pictureType
|
||||
// }
|
||||
// is PictureFrame -> {
|
||||
// pic = entry.pictureData
|
||||
// type = entry.pictureType
|
||||
// }
|
||||
// else -> continue
|
||||
// }
|
||||
//
|
||||
// // Ensure the picture type here is a front cover image so that we don't extract
|
||||
// // an incorrect cover image.
|
||||
// // Yes, this does add some latency, but its quality covers so we can prioritize
|
||||
// // correctness over speed.
|
||||
// if (type == MediaMetadata.PICTURE_TYPE_FRONT_COVER) {
|
||||
// logD("Front cover successfully found")
|
||||
//
|
||||
// // We have a front cover image. Great.
|
||||
// stream = ByteArrayInputStream(pic)
|
||||
// break
|
||||
// } else if (stream != null) {
|
||||
// // In the case a front cover is not found, use the first image in the tag instead.
|
||||
// // This can be corrected later on if a front cover frame is found.
|
||||
// logD("Image not a front cover, assigning image of type $type for now")
|
||||
//
|
||||
// stream = ByteArrayInputStream(pic)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return stream?.use { stm ->
|
||||
// return SourceResult(
|
||||
// source = stm.source().buffer(),
|
||||
// mimeType = context.contentResolver.getType(uri),
|
||||
// dataSource = DataSource.DISK
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
override fun key(data: Album) = data.id.toString()
|
||||
}
|
251
app/src/main/java/org/oxycblt/auxio/coil/AuxioFetcher.kt
Normal file
251
app/src/main/java/org/oxycblt/auxio/coil/AuxioFetcher.kt
Normal file
|
@ -0,0 +1,251 @@
|
|||
package org.oxycblt.auxio.coil
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.media.MediaMetadataRetriever
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.ImageSource
|
||||
import coil.fetch.DrawableResult
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import com.google.android.exoplayer2.MediaItem
|
||||
import com.google.android.exoplayer2.MediaMetadata
|
||||
import com.google.android.exoplayer2.MetadataRetriever
|
||||
import com.google.android.exoplayer2.metadata.flac.PictureFrame
|
||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.toAlbumArtURI
|
||||
import org.oxycblt.auxio.music.toURI
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
abstract class AuxioFetcher : Fetcher {
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
protected suspend fun fetchArt(context: Context, album: Album): InputStream? {
|
||||
if (!settingsManager.showCovers) {
|
||||
return null
|
||||
}
|
||||
|
||||
return if (settingsManager.useQualityCovers) {
|
||||
fetchQualityCovers(context, album)
|
||||
} else {
|
||||
fetchMediaStoreCovers(context, album)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mosaic image from multiple image views, Code adapted from Phonograph
|
||||
* https://github.com/kabouzeid/Phonograph
|
||||
*/
|
||||
protected fun createMosaic(context: Context, streams: List<InputStream>): FetchResult? {
|
||||
if (streams.size < 4) {
|
||||
return streams.getOrNull(0)?.let { stream ->
|
||||
return SourceResult(
|
||||
source = ImageSource(stream.source().buffer(), context),
|
||||
mimeType = null,
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Use a fixed 512x512 canvas for the mosaics. Preferably we would adapt this mosaic to
|
||||
// target ImageView size, but Coil seems to start image loading before we can even get
|
||||
// a width/height for the view, making that impractical.
|
||||
val mosaicBitmap = Bitmap.createBitmap(
|
||||
MOSAIC_BITMAP_SIZE, MOSAIC_BITMAP_SIZE, Bitmap.Config.ARGB_8888
|
||||
)
|
||||
|
||||
val canvas = Canvas(mosaicBitmap)
|
||||
|
||||
var x = 0
|
||||
var y = 0
|
||||
|
||||
// For each stream, create a bitmap scaled to 1/4th of the mosaics combined size
|
||||
// and place it on a corner of the canvas.
|
||||
for (stream in streams) {
|
||||
if (y == MOSAIC_BITMAP_SIZE) {
|
||||
break
|
||||
}
|
||||
|
||||
val bitmap = Bitmap.createScaledBitmap(
|
||||
BitmapFactory.decodeStream(stream),
|
||||
MOSAIC_BITMAP_INCREMENT,
|
||||
MOSAIC_BITMAP_INCREMENT,
|
||||
true
|
||||
)
|
||||
|
||||
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
||||
|
||||
x += MOSAIC_BITMAP_INCREMENT
|
||||
|
||||
if (x == MOSAIC_BITMAP_SIZE) {
|
||||
x = 0
|
||||
y += MOSAIC_BITMAP_INCREMENT
|
||||
}
|
||||
}
|
||||
|
||||
return DrawableResult(
|
||||
drawable = mosaicBitmap.toDrawable(context.resources),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchQualityCovers(context: Context, album: Album): InputStream? {
|
||||
// Loading quality covers basically means to parse the file metadata ourselves
|
||||
// and then extract the cover.
|
||||
|
||||
// First try MediaMetadataRetriever. We will always do this first, as it supports
|
||||
// a variety of formats, has multiple levels of fault tolerance, and is pretty fast
|
||||
// for a manual parser.
|
||||
// However, Samsung seems to cripple this class as to force people to use their ad-infested
|
||||
// music app which relies on proprietary OneUI extensions instead of AOSP. That means
|
||||
// we have to have another layer of redundancy to retain quality. Thanks samsung. Prick.
|
||||
val result = fetchAospMetadataCovers(context, album)
|
||||
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
|
||||
// Our next fallback is to rely on ExoPlayer's largely half-baked and undocumented
|
||||
// metadata system.
|
||||
val exoResult = fetchExoplayerCover(context, album)
|
||||
|
||||
if (exoResult != null) {
|
||||
return exoResult
|
||||
}
|
||||
|
||||
// If the previous two failed, we resort to MediaStore's covers despite it literally
|
||||
// going against the point of this setting. The previous two calls are just too unreliable
|
||||
// and we can't do any filesystem traversing due to scoped storage.
|
||||
val mediaStoreResult = fetchMediaStoreCovers(context, album)
|
||||
|
||||
if (mediaStoreResult != null) {
|
||||
return mediaStoreResult
|
||||
}
|
||||
|
||||
// There is no cover we could feasibly fetch. Give up.
|
||||
return null
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
private suspend fun fetchMediaStoreCovers(context: Context, data: Album): InputStream? {
|
||||
val uri = data.id.toAlbumArtURI()
|
||||
|
||||
// Eliminate any chance that this blocking call might mess up the cancellation process
|
||||
return withContext(Dispatchers.IO) {
|
||||
context.contentResolver.openInputStream(uri)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAospMetadataCovers(context: Context, album: Album): InputStream? {
|
||||
val extractor = MediaMetadataRetriever()
|
||||
|
||||
extractor.use { ext ->
|
||||
// To be safe, just make sure that this blocking call is wrapped so it doesn't
|
||||
// cause problems
|
||||
ext.setDataSource(context, album.songs[0].id.toURI())
|
||||
|
||||
// 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
|
||||
return ext.embeddedPicture?.let { coverBytes ->
|
||||
ByteArrayInputStream(coverBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchExoplayerCover(context: Context, album: Album): InputStream? {
|
||||
val uri = album.songs[0].id.toURI()
|
||||
|
||||
val future = MetadataRetriever.retrieveMetadata(
|
||||
context, MediaItem.fromUri(uri)
|
||||
)
|
||||
|
||||
// future.get is a blocking call that makes the us spin until the future is done.
|
||||
// This is bad for a co-routine, as it prevents cancellation and by extension
|
||||
// messes with the image loading process and causes frustrating bugs.
|
||||
// To fix this we wrap this around in a withContext call to make it suspend and make
|
||||
// sure that the runner can do other coroutines.
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
val tracks = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
future.get()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (tracks == null || tracks.isEmpty) {
|
||||
// Unrecognized format. This is expected, as ExoPlayer only supports a
|
||||
// subset of formats.
|
||||
return null
|
||||
}
|
||||
|
||||
// The metadata extraction process of ExoPlayer is normalized into a superclass.
|
||||
// That means we have to iterate through and find the cover art ourselves.
|
||||
val metadata = tracks[0].getFormat(0).metadata
|
||||
|
||||
if (metadata == null || metadata.length() == 0) {
|
||||
// No (parsable) metadata. This is also expected.
|
||||
return null
|
||||
}
|
||||
|
||||
var stream: ByteArrayInputStream? = null
|
||||
|
||||
for (i in 0 until metadata.length()) {
|
||||
// We can only extract pictures from two tags with this method, ID3v2's APIC or
|
||||
// FLAC's PICTURE.
|
||||
val pic: ByteArray?
|
||||
val type: Int
|
||||
|
||||
when (val entry = metadata.get(i)) {
|
||||
is ApicFrame -> {
|
||||
pic = entry.pictureData
|
||||
type = entry.pictureType
|
||||
}
|
||||
is PictureFrame -> {
|
||||
pic = entry.pictureData
|
||||
type = entry.pictureType
|
||||
}
|
||||
else -> continue
|
||||
}
|
||||
|
||||
// Ensure the picture type here is a front cover image so that we don't extract
|
||||
// an incorrect cover image.
|
||||
// Yes, this does add some latency, but its quality covers so we can prioritize
|
||||
// correctness over speed.
|
||||
if (type == MediaMetadata.PICTURE_TYPE_FRONT_COVER) {
|
||||
logD("Front cover successfully found")
|
||||
|
||||
// We have a front cover image. Great.
|
||||
stream = ByteArrayInputStream(pic)
|
||||
break
|
||||
} else if (stream != null) {
|
||||
// In the case a front cover is not found, use the first image in the tag instead.
|
||||
// This can be corrected later on if a front cover frame is found.
|
||||
logD("Image not a front cover, assigning image of type $type for now")
|
||||
|
||||
stream = ByteArrayInputStream(pic)
|
||||
}
|
||||
}
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MOSAIC_BITMAP_SIZE = 512
|
||||
private const val MOSAIC_BITMAP_INCREMENT = 256
|
||||
}
|
||||
}
|
|
@ -21,21 +21,18 @@ package org.oxycblt.auxio.coil
|
|||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.databinding.BindingAdapter
|
||||
import coil.Coil
|
||||
import coil.clear
|
||||
import coil.fetch.Fetcher
|
||||
import coil.dispose
|
||||
import coil.imageLoader
|
||||
import coil.load
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.OriginalSize
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.BaseModel
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
|
||||
// --- BINDING ADAPTERS ---
|
||||
|
||||
|
@ -44,7 +41,11 @@ import org.oxycblt.auxio.settings.SettingsManager
|
|||
*/
|
||||
@BindingAdapter("albumArt")
|
||||
fun ImageView.bindAlbumArt(song: Song?) {
|
||||
load(song?.album, R.drawable.ic_album, AlbumArtFetcher(context))
|
||||
dispose()
|
||||
|
||||
load(song) {
|
||||
error(R.drawable.ic_album)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,7 +53,11 @@ fun ImageView.bindAlbumArt(song: Song?) {
|
|||
*/
|
||||
@BindingAdapter("albumArt")
|
||||
fun ImageView.bindAlbumArt(album: Album?) {
|
||||
load(album, R.drawable.ic_album, AlbumArtFetcher(context))
|
||||
dispose()
|
||||
|
||||
load(album) {
|
||||
error(R.drawable.ic_album)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,7 +65,11 @@ fun ImageView.bindAlbumArt(album: Album?) {
|
|||
*/
|
||||
@BindingAdapter("artistImage")
|
||||
fun ImageView.bindArtistImage(artist: Artist?) {
|
||||
load(artist, R.drawable.ic_artist, MosaicFetcher(context))
|
||||
dispose()
|
||||
|
||||
load(artist) {
|
||||
error(R.drawable.ic_artist)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -68,32 +77,11 @@ fun ImageView.bindArtistImage(artist: Artist?) {
|
|||
*/
|
||||
@BindingAdapter("genreImage")
|
||||
fun ImageView.bindGenreImage(genre: Genre?) {
|
||||
load(genre, R.drawable.ic_genre, MosaicFetcher(context))
|
||||
}
|
||||
dispose()
|
||||
|
||||
/**
|
||||
* Custom extension function similar to the stock coil load extensions, but handles whether
|
||||
* to show images and custom fetchers.
|
||||
* @param T Any datatype that inherits [BaseModel]. This can be null, but keep in mind that it will cause loading to fail.
|
||||
* @param data The data itself
|
||||
* @param error Drawable resource to use when loading failed/should not occur.
|
||||
* @param fetcher Required fetcher that uses [T] as its datatype
|
||||
*/
|
||||
inline fun <reified T : BaseModel> ImageView.load(
|
||||
data: T?,
|
||||
@DrawableRes error: Int,
|
||||
fetcher: Fetcher<T>,
|
||||
) {
|
||||
clear()
|
||||
|
||||
Coil.imageLoader(context).enqueue(
|
||||
ImageRequest.Builder(context)
|
||||
.target(this)
|
||||
.data(data)
|
||||
.fetcher(fetcher)
|
||||
.error(error)
|
||||
.build()
|
||||
)
|
||||
load(genre?.songs?.get(0)?.album) {
|
||||
error(R.drawable.ic_genre)
|
||||
}
|
||||
}
|
||||
|
||||
// --- OTHER FUNCTIONS ---
|
||||
|
@ -108,17 +96,9 @@ fun loadBitmap(
|
|||
song: Song,
|
||||
onDone: (Bitmap?) -> Unit
|
||||
) {
|
||||
val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
if (!settingsManager.showCovers) {
|
||||
onDone(null)
|
||||
return
|
||||
}
|
||||
|
||||
Coil.imageLoader(context).enqueue(
|
||||
context.imageLoader.enqueue(
|
||||
ImageRequest.Builder(context)
|
||||
.data(song.album)
|
||||
.fetcher(AlbumArtFetcher(context))
|
||||
.size(OriginalSize)
|
||||
.target(
|
||||
onError = { onDone(null) },
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package org.oxycblt.auxio.coil
|
||||
|
||||
import android.widget.ImageView
|
||||
import coil.decode.DataSource
|
||||
import coil.drawable.CrossfadeDrawable
|
||||
import coil.request.ErrorResult
|
||||
import coil.request.ImageResult
|
||||
import coil.request.SuccessResult
|
||||
import coil.size.Scale
|
||||
import coil.transition.Transition
|
||||
import coil.transition.TransitionTarget
|
||||
|
||||
/**
|
||||
* A modified variant of coil's CrossfadeTransition that actually animates error results.
|
||||
* You know. Like it used to.
|
||||
*
|
||||
* @author Coil Team
|
||||
*/
|
||||
class CrossfadeTransition @JvmOverloads constructor(
|
||||
private val target: TransitionTarget,
|
||||
private val result: ImageResult,
|
||||
private val durationMillis: Int = CrossfadeDrawable.DEFAULT_DURATION,
|
||||
private val preferExactIntrinsicSize: Boolean = false
|
||||
) : Transition {
|
||||
|
||||
init {
|
||||
require(durationMillis > 0) { "durationMillis must be > 0." }
|
||||
}
|
||||
|
||||
override fun transition() {
|
||||
val drawable = CrossfadeDrawable(
|
||||
start = target.drawable,
|
||||
end = result.drawable,
|
||||
scale = (target.view as? ImageView)?.scale ?: Scale.FIT,
|
||||
durationMillis = durationMillis,
|
||||
fadeStart = !(result is SuccessResult && result.isPlaceholderCached),
|
||||
preferExactIntrinsicSize = preferExactIntrinsicSize
|
||||
)
|
||||
|
||||
when (result) {
|
||||
is SuccessResult -> target.onSuccess(drawable)
|
||||
is ErrorResult -> target.onError(drawable)
|
||||
}
|
||||
}
|
||||
|
||||
val ImageView.scale: Scale
|
||||
get() = when (scaleType) {
|
||||
ImageView.ScaleType.FIT_START, ImageView.ScaleType.FIT_CENTER,
|
||||
ImageView.ScaleType.FIT_END, ImageView.ScaleType.CENTER_INSIDE -> Scale.FIT
|
||||
else -> Scale.FILL
|
||||
}
|
||||
|
||||
class Factory @JvmOverloads constructor(
|
||||
val durationMillis: Int = CrossfadeDrawable.DEFAULT_DURATION,
|
||||
val preferExactIntrinsicSize: Boolean = false
|
||||
) : Transition.Factory {
|
||||
|
||||
init {
|
||||
require(durationMillis > 0) { "durationMillis must be > 0." }
|
||||
}
|
||||
|
||||
override fun create(target: TransitionTarget, result: ImageResult): Transition {
|
||||
// !!!!!!!!!!!!!! MODIFICATION !!!!!!!!!!!!!!
|
||||
// Remove the error check for this transition. Usually when something errors in
|
||||
// Auxio it will stay erroring, so not crossfading on an error looks weird.
|
||||
|
||||
// Don't animate if the request was fulfilled by the memory cache.
|
||||
if (result is SuccessResult && result.dataSource == DataSource.MEMORY_CACHE) {
|
||||
return Transition.Factory.NONE.create(target, result)
|
||||
}
|
||||
|
||||
return CrossfadeTransition(target, result, durationMillis, preferExactIntrinsicSize)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
return other is Factory &&
|
||||
durationMillis == other.durationMillis &&
|
||||
preferExactIntrinsicSize == other.preferExactIntrinsicSize
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = durationMillis
|
||||
result = 31 * result + preferExactIntrinsicSize.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
128
app/src/main/java/org/oxycblt/auxio/coil/Fetchers.kt
Normal file
128
app/src/main/java/org/oxycblt/auxio/coil/Fetchers.kt
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Auxio Project
|
||||
* Fetchers.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.coil
|
||||
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.ImageSource
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.request.Options
|
||||
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.Song
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Fetcher that returns the album art for a given [Album]. Handles settings on whether to use
|
||||
* quality covers or not.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AlbumArtFetcher private constructor(
|
||||
private val context: Context,
|
||||
private val album: Album
|
||||
) : AuxioFetcher() {
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
return fetchArt(context, album)?.let { stream ->
|
||||
SourceResult(
|
||||
source = ImageSource(stream.source().buffer(), context),
|
||||
mimeType = null,
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SongFactory : Fetcher.Factory<Song> {
|
||||
override fun create(data: Song, options: Options, imageLoader: ImageLoader): Fetcher? {
|
||||
return AlbumArtFetcher(options.context, data.album)
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumFactory : Fetcher.Factory<Album> {
|
||||
override fun create(data: Album, options: Options, imageLoader: ImageLoader): Fetcher? {
|
||||
return AlbumArtFetcher(options.context, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistImageFetcher private constructor(
|
||||
private val context: Context,
|
||||
private val artist: Artist
|
||||
) : AuxioFetcher() {
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
val end = min(4, artist.albums.size)
|
||||
val results = artist.albums.mapN(end) { album ->
|
||||
fetchArt(context, album)
|
||||
}
|
||||
|
||||
return createMosaic(context, results)
|
||||
}
|
||||
|
||||
class Factory : Fetcher.Factory<Artist> {
|
||||
override fun create(data: Artist, options: Options, imageLoader: ImageLoader): Fetcher? {
|
||||
return ArtistImageFetcher(options.context, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GenreImageFetcher private constructor(
|
||||
private val context: Context,
|
||||
private val genre: Genre
|
||||
) : AuxioFetcher() {
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
val albums = genre.songs.groupBy { it.album }.keys
|
||||
val end = min(4, albums.size)
|
||||
val results = albums.mapN(end) { album ->
|
||||
fetchArt(context, album)
|
||||
}
|
||||
|
||||
return createMosaic(context, results)
|
||||
}
|
||||
|
||||
class Factory : Fetcher.Factory<Genre> {
|
||||
override fun create(data: Genre, options: Options, imageLoader: ImageLoader): Fetcher? {
|
||||
return GenreImageFetcher(options.context, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map only [n] items from a collection. [transform] is called for each item that is eligible.
|
||||
* If null is returned, then that item will be skipped.
|
||||
*/
|
||||
private inline fun <T : Any, R : Any> Iterable<T>.mapN(n: Int, transform: (T) -> R?): List<R> {
|
||||
val out = mutableListOf<R>()
|
||||
|
||||
for (item in this) {
|
||||
if (out.size >= n) {
|
||||
break
|
||||
}
|
||||
|
||||
transform(item)?.let {
|
||||
out.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Auxio Project
|
||||
* MosaicFetcher.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.coil
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil.bitmap.BitmapPool
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.Options
|
||||
import coil.fetch.DrawableResult
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.size.OriginalSize
|
||||
import coil.size.Size
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import java.lang.Exception
|
||||
|
||||
/**
|
||||
* A [Fetcher] that takes an [Artist] or [Genre] and returns a mosaic of its albums.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class MosaicFetcher(private val context: Context) : Fetcher<MusicParent> {
|
||||
override suspend fun fetch(
|
||||
pool: BitmapPool,
|
||||
data: MusicParent,
|
||||
size: Size,
|
||||
options: Options
|
||||
): FetchResult {
|
||||
// Get the URIs for either a genre or artist
|
||||
val albums = mutableListOf<Album>()
|
||||
|
||||
when (data) {
|
||||
is Artist -> data.albums.forEachIndexed { index, album ->
|
||||
if (index < 4) { albums.add(album) }
|
||||
}
|
||||
|
||||
is Genre -> data.songs.groupBy { it.album }.keys.forEachIndexed { index, album ->
|
||||
if (index < 4) { albums.add(album) }
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
// Fetch our cover art using AlbumArtFetcher, as that respects any settings and is
|
||||
// generally resilient to frustrating MediaStore issues
|
||||
val results = mutableListOf<SourceResult>()
|
||||
val artFetcher = AlbumArtFetcher(context)
|
||||
|
||||
// Load MediaStore streams
|
||||
albums.forEach { album ->
|
||||
try {
|
||||
results.add(artFetcher.fetch(pool, album, OriginalSize, options) as SourceResult)
|
||||
} catch (e: Exception) {
|
||||
// Whatever.
|
||||
}
|
||||
}
|
||||
|
||||
// If so many fetches failed that there's not enough images to make a mosaic, then
|
||||
// just return the first cover image.
|
||||
if (results.size < 4) {
|
||||
// Dont even bother if ALL the streams have failed.
|
||||
check(results.isNotEmpty()) { "All streams have failed. " }
|
||||
|
||||
return results[0]
|
||||
}
|
||||
|
||||
val bitmap = drawMosaic(results)
|
||||
|
||||
return DrawableResult(
|
||||
drawable = bitmap.toDrawable(context.resources),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the mosaic image, Code adapted from Phonograph
|
||||
* https://github.com/kabouzeid/Phonograph
|
||||
*/
|
||||
private fun drawMosaic(results: List<SourceResult>): Bitmap {
|
||||
// Use a fixed 512x512 canvas for the mosaics. Preferably we would adapt this mosaic to
|
||||
// target ImageView size, but Coil seems to start image loading before we can even get
|
||||
// a width/height for the view, making that impractical.
|
||||
val mosaicBitmap = Bitmap.createBitmap(
|
||||
MOSAIC_BITMAP_SIZE, MOSAIC_BITMAP_SIZE, Bitmap.Config.ARGB_8888
|
||||
)
|
||||
|
||||
val canvas = Canvas(mosaicBitmap)
|
||||
|
||||
var x = 0
|
||||
var y = 0
|
||||
|
||||
// For each stream, create a bitmap scaled to 1/4th of the mosaics combined size
|
||||
// and place it on a corner of the canvas.
|
||||
results.forEach { result ->
|
||||
if (y == MOSAIC_BITMAP_SIZE) return@forEach
|
||||
|
||||
val bitmap = Bitmap.createScaledBitmap(
|
||||
BitmapFactory.decodeStream(result.source.inputStream()),
|
||||
MOSAIC_BITMAP_INCREMENT,
|
||||
MOSAIC_BITMAP_INCREMENT,
|
||||
true
|
||||
)
|
||||
|
||||
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
||||
|
||||
x += MOSAIC_BITMAP_INCREMENT
|
||||
|
||||
if (x == MOSAIC_BITMAP_SIZE) {
|
||||
x = 0
|
||||
y += MOSAIC_BITMAP_INCREMENT
|
||||
}
|
||||
}
|
||||
|
||||
return mosaicBitmap
|
||||
}
|
||||
|
||||
override fun key(data: MusicParent): String = data.hashCode().toString()
|
||||
override fun handles(data: MusicParent) = data !is Album // Albums are not used here
|
||||
|
||||
companion object {
|
||||
private const val MOSAIC_BITMAP_SIZE = 512
|
||||
private const val MOSAIC_BITMAP_INCREMENT = 256
|
||||
}
|
||||
}
|
14
app/src/main/java/org/oxycblt/auxio/coil/MusicKeyer.kt
Normal file
14
app/src/main/java/org/oxycblt/auxio/coil/MusicKeyer.kt
Normal file
|
@ -0,0 +1,14 @@
|
|||
package org.oxycblt.auxio.coil
|
||||
|
||||
import coil.key.Keyer
|
||||
import coil.request.Options
|
||||
import org.oxycblt.auxio.music.Music
|
||||
|
||||
/**
|
||||
* A basic keyer for music data.
|
||||
*/
|
||||
class MusicKeyer : Keyer<Music> {
|
||||
override fun key(data: Music, options: Options): String? {
|
||||
return "${data::class.simpleName}: ${data.id}"
|
||||
}
|
||||
}
|
|
@ -118,6 +118,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.MusicCal
|
|||
settingsManager.libGenreSort = sort
|
||||
mGenres.value = sort.sortParents(mGenres.value!!)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,6 @@ import kotlin.math.abs
|
|||
* - Added drag listener
|
||||
* - TODO: Added documentation
|
||||
* - TODO: Popup will center itself to the thumb when possible
|
||||
* - TODO: Stabilize how I'm using padding
|
||||
*/
|
||||
class FastScrollRecyclerView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
|
|
|
@ -49,7 +49,7 @@ class PlaybackBarLayout @JvmOverloads constructor(
|
|||
@AttrRes defStyleAttr: Int = 0,
|
||||
@StyleRes defStyleRes: Int = 0
|
||||
) : ViewGroup(context, attrs, defStyleAttr, defStyleRes) {
|
||||
private val playbackView = CompactPlaybackView(context)
|
||||
private val playbackView = PlaybackBarView(context)
|
||||
private var barDragHelper = ViewDragHelper.create(this, BarDragCallback())
|
||||
private var lastInsets: WindowInsets? = null
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ import org.oxycblt.auxio.util.systemBarsCompat
|
|||
* A view displaying the playback state in a compact manner. This is only meant to be used
|
||||
* by [PlaybackBarLayout].
|
||||
*/
|
||||
class CompactPlaybackView @JvmOverloads constructor(
|
||||
class PlaybackBarView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = -1
|
||||
|
@ -60,7 +60,7 @@ class CompactPlaybackView @JvmOverloads constructor(
|
|||
// MaterialShapeDrawable at runtime and allowing this code to work on API 21.
|
||||
background = R.drawable.ui_shape_ripple.resolveDrawable(context).apply {
|
||||
val backgroundDrawable = MaterialShapeDrawable.createWithElevationOverlay(context).apply {
|
||||
elevation = this@CompactPlaybackView.elevation
|
||||
elevation = this@PlaybackBarView.elevation
|
||||
fillColor = ColorStateList.valueOf(R.attr.colorSurface.resolveAttr(context))
|
||||
}
|
||||
|
|
@ -88,6 +88,7 @@ class SearchAdapter(
|
|||
is Album -> (holder as AlbumViewHolder).bind(item)
|
||||
is Song -> (holder as SongViewHolder).bind(item)
|
||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,8 +148,7 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
|||
SettingsManager.KEY_SHOW_COVERS, SettingsManager.KEY_QUALITY_COVERS -> {
|
||||
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ ->
|
||||
Coil.imageLoader(requireContext()).apply {
|
||||
bitmapPool.clear()
|
||||
memoryCache.clear()
|
||||
this.memoryCache?.clear()
|
||||
}
|
||||
|
||||
true
|
||||
|
|
|
@ -35,7 +35,6 @@ import coil.request.ImageRequest
|
|||
import coil.transform.RoundedCornersTransformation
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.coil.AlbumArtFetcher
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.util.isLandscape
|
||||
|
@ -98,7 +97,6 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
|
||||
val coverRequest = ImageRequest.Builder(context)
|
||||
.data(song.album)
|
||||
.fetcher(AlbumArtFetcher(context))
|
||||
.size(imageSize)
|
||||
|
||||
// If we are on Android 12 or higher, round out the album cover so that the widget is
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:overScrollMode="never"
|
||||
android:paddingTop="@dimen/spacing_medium"
|
||||
android:paddingTop="@dimen/spacing_small"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toTopOf="@+id/accent_cancel"
|
||||
app:layout_constraintTop_toBottomOf="@+id/accent_header"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<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.CompactPlaybackView">
|
||||
tools:context=".playback.PlaybackBarView">
|
||||
|
||||
<data>
|
||||
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
android:paddingEnd="@dimen/spacing_small"
|
||||
app:labelBehavior="gone"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="0"
|
||||
app:thumbRadius="6dp"
|
||||
app:haloRadius="14dp"
|
||||
android:valueTo="1"
|
||||
app:thumbRadius="@dimen/slider_thumb_radius"
|
||||
app:haloRadius="@dimen/slider_halo_radius"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
|
|
@ -30,6 +30,9 @@
|
|||
<dimen name="popup_min_width">78dp</dimen>
|
||||
<dimen name="popup_padding_end">28dp</dimen>
|
||||
|
||||
<dimen name="slider_thumb_radius">6dp</dimen>
|
||||
<dimen name="slider_halo_radius">12dp</dimen>
|
||||
|
||||
<dimen name="widget_width_min">176dp</dimen>
|
||||
<dimen name="widget_height_min">110dp</dimen>
|
||||
<dimen name="widget_width_def">@dimen/widget_width_min</dimen>
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
<!-- Custom dialog title theme -->
|
||||
<style name="Widget.Auxio.Dialog.TextView" parent="MaterialAlertDialog.Material3.Title.Text">
|
||||
<item name="android:fontFamily">@font/inter_semibold</item>
|
||||
<item name="android:textAppearance">@style/TextAppearance.Auxio.TitleMidLarge</item>
|
||||
</style>
|
||||
|
||||
|
|
|
@ -135,7 +135,6 @@
|
|||
</style>
|
||||
|
||||
<style name="Widget.Auxio.Button.Primary" parent="Widget.Material3.Button">
|
||||
<!-- TODO: Report elevation bug -->
|
||||
<item name="android:textAppearance">@style/TextAppearance.Auxio.LabelLarger</item>
|
||||
<item name="android:stateListAnimator">@null</item>
|
||||
<item name="android:elevation">0dp</item>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.5.31"
|
||||
ext.kotlin_version = '1.6.0'
|
||||
ext.navigation_version = "2.3.5"
|
||||
|
||||
repositories {
|
||||
|
|
Loading…
Reference in a new issue