image: add option to restore 1:1 crop behavior
Add an option to restore the old 1:1 crop behavior to the app. Some people think this looks better, some people like to have youtube thumbnails in their APICs. Can't really be opinionated here.
This commit is contained in:
parent
f1470af586
commit
903a3e561a
10 changed files with 110 additions and 34 deletions
|
@ -48,6 +48,8 @@ import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.image.extractor.RoundedRectTransformation
|
||||||
|
import org.oxycblt.auxio.image.extractor.SquareCropTransformation
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
@ -77,6 +79,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
FrameLayout(context, attrs, defStyleAttr) {
|
FrameLayout(context, attrs, defStyleAttr) {
|
||||||
@Inject lateinit var imageLoader: ImageLoader
|
@Inject lateinit var imageLoader: ImageLoader
|
||||||
@Inject lateinit var uiSettings: UISettings
|
@Inject lateinit var uiSettings: UISettings
|
||||||
|
@Inject lateinit var imageSettings: ImageSettings
|
||||||
|
|
||||||
private val image: ImageView
|
private val image: ImageView
|
||||||
|
|
||||||
|
@ -384,13 +387,19 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
.data(songs)
|
.data(songs)
|
||||||
.error(StyledDrawable(context, context.getDrawableCompat(errorRes), iconSizeRes))
|
.error(StyledDrawable(context, context.getDrawableCompat(errorRes), iconSizeRes))
|
||||||
.transformations(
|
|
||||||
RoundedCornersTransformation(cornerRadiusRes?.let(context::getDimen) ?: 0f))
|
|
||||||
.target(image)
|
.target(image)
|
||||||
.build()
|
|
||||||
|
val cornersTransformation =
|
||||||
|
RoundedRectTransformation(cornerRadiusRes?.let(context::getDimen) ?: 0f)
|
||||||
|
if (imageSettings.forceSquareCovers) {
|
||||||
|
request.transformations(SquareCropTransformation.INSTANCE, cornersTransformation)
|
||||||
|
} else {
|
||||||
|
request.transformations(cornersTransformation)
|
||||||
|
}
|
||||||
|
|
||||||
// Dispose of any previous image request and load a new image.
|
// Dispose of any previous image request and load a new image.
|
||||||
CoilUtils.dispose(image)
|
CoilUtils.dispose(image)
|
||||||
imageLoader.enqueue(request)
|
imageLoader.enqueue(request.build())
|
||||||
contentDescription = desc
|
contentDescription = desc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,8 @@ import org.oxycblt.auxio.util.logD
|
||||||
interface ImageSettings : Settings<ImageSettings.Listener> {
|
interface ImageSettings : Settings<ImageSettings.Listener> {
|
||||||
/** The strategy to use when loading album covers. */
|
/** The strategy to use when loading album covers. */
|
||||||
val coverMode: CoverMode
|
val coverMode: CoverMode
|
||||||
|
/** Whether to force all album covers to have a 1:1 aspect ratio. */
|
||||||
|
val forceSquareCovers: Boolean
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
/** Called when [coverMode] changes. */
|
/** Called when [coverMode] changes. */
|
||||||
|
@ -49,6 +51,9 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context
|
||||||
sharedPreferences.getInt(getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
|
sharedPreferences.getInt(getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
|
||||||
?: CoverMode.MEDIA_STORE
|
?: CoverMode.MEDIA_STORE
|
||||||
|
|
||||||
|
override val forceSquareCovers: Boolean
|
||||||
|
get() = sharedPreferences.getBoolean(getString(R.string.set_key_square_covers), false)
|
||||||
|
|
||||||
override fun migrate() {
|
override fun migrate() {
|
||||||
// Show album covers and Ignore MediaStore covers were unified in 3.0.0
|
// Show album covers and Ignore MediaStore covers were unified in 3.0.0
|
||||||
if (sharedPreferences.contains(OLD_KEY_SHOW_COVERS) ||
|
if (sharedPreferences.contains(OLD_KEY_SHOW_COVERS) ||
|
||||||
|
|
|
@ -43,7 +43,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.min
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.guava.asDeferred
|
import kotlinx.coroutines.guava.asDeferred
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -155,7 +154,7 @@ constructor(
|
||||||
// Get the embedded picture from MediaMetadataRetriever, which will return a full
|
// Get the embedded picture from MediaMetadataRetriever, which will return a full
|
||||||
// ByteArray of the cover without any compression artifacts.
|
// ByteArray of the cover without any compression artifacts.
|
||||||
// If its null [i.e there is no embedded cover], than just ignore it and move on
|
// If its null [i.e there is no embedded cover], than just ignore it and move on
|
||||||
return embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() }
|
embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun extractExoplayerCover(album: Album): InputStream? {
|
private suspend fun extractExoplayerCover(album: Album): InputStream? {
|
||||||
|
@ -212,7 +211,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
|
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
|
||||||
private fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {
|
private suspend fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {
|
||||||
// Use whatever size coil gives us to create the mosaic.
|
// Use whatever size coil gives us to create the mosaic.
|
||||||
val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize())
|
val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize())
|
||||||
val mosaicFrameSize =
|
val mosaicFrameSize =
|
||||||
|
@ -234,7 +233,9 @@ constructor(
|
||||||
|
|
||||||
// Crop the bitmap down to a square so it leaves no empty space
|
// Crop the bitmap down to a square so it leaves no empty space
|
||||||
// TODO: Work around this
|
// TODO: Work around this
|
||||||
val bitmap = cropBitmap(BitmapFactory.decodeStream(stream), mosaicFrameSize)
|
val bitmap =
|
||||||
|
SquareCropTransformation.INSTANCE.transform(
|
||||||
|
BitmapFactory.decodeStream(stream), mosaicFrameSize)
|
||||||
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
||||||
|
|
||||||
x += bitmap.width
|
x += bitmap.width
|
||||||
|
@ -259,21 +260,4 @@ constructor(
|
||||||
val size = pxOrElse { 512 }
|
val size = pxOrElse { 512 }
|
||||||
return if (size.mod(2) > 0) size + 1 else size
|
return if (size.mod(2) > 0) size + 1 else size
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cropBitmap(input: Bitmap, size: Size): Bitmap {
|
|
||||||
// Find the smaller dimension and then take a center portion of the image that
|
|
||||||
// has that size.
|
|
||||||
val dstSize = min(input.width, input.height)
|
|
||||||
val x = (input.width - dstSize) / 2
|
|
||||||
val y = (input.height - dstSize) / 2
|
|
||||||
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
|
|
||||||
|
|
||||||
val desiredWidth = size.width.pxOrElse { dstSize }
|
|
||||||
val desiredHeight = size.height.pxOrElse { dstSize }
|
|
||||||
if (dstSize != desiredWidth || dstSize != desiredHeight) {
|
|
||||||
// Image is not the desired size, upscale it.
|
|
||||||
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
|
|
||||||
}
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 Auxio Project
|
* Copyright (c) 2023 Auxio Project
|
||||||
* RoundedCornersTransformation.kt is part of Auxio.
|
* RoundedRectTransformation.kt is part of Auxio.
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.image
|
package org.oxycblt.auxio.image.extractor
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Bitmap.createBitmap
|
import android.graphics.Bitmap.createBitmap
|
||||||
|
@ -43,7 +43,7 @@ import kotlin.math.roundToInt
|
||||||
*
|
*
|
||||||
* @author Coil Team, Alexander Capehart (OxygenCobalt)
|
* @author Coil Team, Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class RoundedCornersTransformation(
|
class RoundedRectTransformation(
|
||||||
@Px private val topLeft: Float = 0f,
|
@Px private val topLeft: Float = 0f,
|
||||||
@Px private val topRight: Float = 0f,
|
@Px private val topRight: Float = 0f,
|
||||||
@Px private val bottomLeft: Float = 0f,
|
@Px private val bottomLeft: Float = 0f,
|
||||||
|
@ -122,7 +122,7 @@ class RoundedCornersTransformation(
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
return other is RoundedCornersTransformation &&
|
return other is RoundedRectTransformation &&
|
||||||
topLeft == other.topLeft &&
|
topLeft == other.topLeft &&
|
||||||
topRight == other.topRight &&
|
topRight == other.topRight &&
|
||||||
bottomLeft == other.bottomLeft &&
|
bottomLeft == other.bottomLeft &&
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* SquareCropTransformation.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.image.extractor
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import coil.size.Size
|
||||||
|
import coil.size.pxOrElse
|
||||||
|
import coil.transform.Transformation
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Transformation] that performs a center crop-style transformation on an image. Allowing this
|
||||||
|
* behavior to be intrinsic without any view configuration.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
class SquareCropTransformation : Transformation {
|
||||||
|
override val cacheKey: String
|
||||||
|
get() = "SquareCropTransformation"
|
||||||
|
|
||||||
|
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
|
||||||
|
// Find the smaller dimension and then take a center portion of the image that
|
||||||
|
// has that size.
|
||||||
|
val dstSize = min(input.width, input.height)
|
||||||
|
val x = (input.width - dstSize) / 2
|
||||||
|
val y = (input.height - dstSize) / 2
|
||||||
|
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
|
||||||
|
|
||||||
|
val desiredWidth = size.width.pxOrElse { dstSize }
|
||||||
|
val desiredHeight = size.height.pxOrElse { dstSize }
|
||||||
|
if (dstSize != desiredWidth || dstSize != desiredHeight) {
|
||||||
|
// Image is not the desired size, upscale it.
|
||||||
|
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** A re-usable instance. */
|
||||||
|
val INSTANCE = SquareCropTransformation()
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,7 +47,8 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSetupPreference(preference: Preference) {
|
override fun onSetupPreference(preference: Preference) {
|
||||||
if (preference.key == getString(R.string.set_key_cover_mode)) {
|
if (preference.key == getString(R.string.set_key_cover_mode) ||
|
||||||
|
preference.key == getString(R.string.set_key_square_covers)) {
|
||||||
logD("Configuring cover mode setting")
|
logD("Configuring cover mode setting")
|
||||||
preference.onPreferenceChangeListener =
|
preference.onPreferenceChangeListener =
|
||||||
Preference.OnPreferenceChangeListener { _, _ ->
|
Preference.OnPreferenceChangeListener { _, _ ->
|
||||||
|
|
|
@ -27,7 +27,8 @@ import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.image.BitmapProvider
|
import org.oxycblt.auxio.image.BitmapProvider
|
||||||
import org.oxycblt.auxio.image.ImageSettings
|
import org.oxycblt.auxio.image.ImageSettings
|
||||||
import org.oxycblt.auxio.image.RoundedCornersTransformation
|
import org.oxycblt.auxio.image.extractor.RoundedRectTransformation
|
||||||
|
import org.oxycblt.auxio.image.extractor.SquareCropTransformation
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.queue.Queue
|
import org.oxycblt.auxio.playback.queue.Queue
|
||||||
|
@ -98,10 +99,19 @@ constructor(
|
||||||
return if (cornerRadius > 0) {
|
return if (cornerRadius > 0) {
|
||||||
// If rounded, reduce the bitmap size further to obtain more pronounced
|
// If rounded, reduce the bitmap size further to obtain more pronounced
|
||||||
// rounded corners.
|
// rounded corners.
|
||||||
builder
|
builder.size(getSafeRemoteViewsImageSize(context, 10f))
|
||||||
.size(getSafeRemoteViewsImageSize(context, 10f))
|
val cornersTransformation =
|
||||||
.transformations(RoundedCornersTransformation(cornerRadius.toFloat()))
|
RoundedRectTransformation(cornerRadius.toFloat())
|
||||||
|
if (imageSettings.forceSquareCovers) {
|
||||||
|
builder.transformations(
|
||||||
|
SquareCropTransformation.INSTANCE, cornersTransformation)
|
||||||
|
} else {
|
||||||
|
builder.transformations(cornersTransformation)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (imageSettings.forceSquareCovers) {
|
||||||
|
builder.transformations(SquareCropTransformation.INSTANCE)
|
||||||
|
}
|
||||||
builder.size(getSafeRemoteViewsImageSize(context))
|
builder.size(getSafeRemoteViewsImageSize(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
<string name="set_key_observing" translatable="false">auxio_observing</string>
|
<string name="set_key_observing" translatable="false">auxio_observing</string>
|
||||||
<string name="set_key_music_dirs" translatable="false">auxio_music_dirs</string>
|
<string name="set_key_music_dirs" translatable="false">auxio_music_dirs</string>
|
||||||
<string name="set_key_cover_mode" translatable="false">auxio_cover_mode</string>
|
<string name="set_key_cover_mode" translatable="false">auxio_cover_mode</string>
|
||||||
|
<string name="set_key_square_covers" translatable="false">auxio_square_covers</string>
|
||||||
<string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string>
|
<string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string>
|
||||||
<string name="set_key_exclude_non_music" translatable="false">auxio_exclude_non_music</string>
|
<string name="set_key_exclude_non_music" translatable="false">auxio_exclude_non_music</string>
|
||||||
<string name="set_key_separators" translatable="false">auxio_separators</string>
|
<string name="set_key_separators" translatable="false">auxio_separators</string>
|
||||||
|
|
|
@ -239,6 +239,8 @@
|
||||||
<string name="set_cover_mode_off">Off</string>
|
<string name="set_cover_mode_off">Off</string>
|
||||||
<string name="set_cover_mode_media_store">Fast</string>
|
<string name="set_cover_mode_media_store">Fast</string>
|
||||||
<string name="set_cover_mode_quality">High quality</string>
|
<string name="set_cover_mode_quality">High quality</string>
|
||||||
|
<string name="set_square_covers">Force square album covers</string>
|
||||||
|
<string name="set_square_covers_desc">Crop all album covers to a 1:1 aspect ratio</string>
|
||||||
|
|
||||||
<string name="set_audio">Audio</string>
|
<string name="set_audio">Audio</string>
|
||||||
<string name="set_audio_desc">Configure sound and playback behavior</string>
|
<string name="set_audio_desc">Configure sound and playback behavior</string>
|
||||||
|
|
|
@ -43,5 +43,11 @@
|
||||||
app:key="@string/set_key_cover_mode"
|
app:key="@string/set_key_cover_mode"
|
||||||
app:title="@string/set_cover_mode" />
|
app:title="@string/set_cover_mode" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:defaultValue="false"
|
||||||
|
app:key="@string/set_key_square_covers"
|
||||||
|
app:summary="@string/set_square_covers_desc"
|
||||||
|
app:title="@string/set_square_covers" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
Loading…
Reference in a new issue