app: cleanup
This commit is contained in:
parent
343856ac69
commit
aac6d8ef4d
38 changed files with 76 additions and 228 deletions
|
@ -18,8 +18,8 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId namespace
|
||||
versionName "4.0.2"
|
||||
versionCode 61
|
||||
versionName "4.0.3"
|
||||
versionCode 62
|
||||
|
||||
minSdk min_sdk
|
||||
targetSdk target_sdk
|
||||
|
|
|
@ -1309,7 +1309,6 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
|||
+ " should not be set externally.");
|
||||
}
|
||||
if (!hideable && state == STATE_HIDDEN) {
|
||||
Log.w(TAG, "Cannot set state: " + state);
|
||||
return;
|
||||
}
|
||||
final int finalState;
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
package org.oxycblt.auxio
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewTreeObserver
|
||||
|
@ -514,8 +513,6 @@ class MainFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private var scrimAnimator: ValueAnimator? = null
|
||||
|
||||
private fun updateSpeedDial(open: Boolean) {
|
||||
requireNotNull(speedDialBackCallback) { "SpeedDialBackPressedCallback was not available" }
|
||||
.invalidateEnabled(open)
|
||||
|
|
|
@ -98,7 +98,7 @@ sealed interface ArtistShowChoices {
|
|||
val uid: Music.UID
|
||||
/** The current [Artist] choices. */
|
||||
val choices: List<Artist>
|
||||
/** Sanitize this instance with a [DeviceLibrary]. */
|
||||
/** Sanitize this instance with a [Library]. */
|
||||
fun sanitize(newLibrary: Library): ArtistShowChoices?
|
||||
|
||||
/** Backing implementation of [ArtistShowChoices] that is based on a [Song]. */
|
||||
|
|
|
@ -37,12 +37,10 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import kotlin.math.abs
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeBinding
|
||||
|
@ -68,7 +66,6 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
|||
import org.oxycblt.auxio.util.collect
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.lazyReflectedField
|
||||
import org.oxycblt.auxio.util.lazyReflectedMethod
|
||||
import org.oxycblt.auxio.util.navigateSafe
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
import org.oxycblt.musikr.IndexingProgress
|
||||
|
@ -94,7 +91,6 @@ class HomeFragment :
|
|||
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null
|
||||
private var getContentLauncher: ActivityResultLauncher<String>? = null
|
||||
private var pendingImportTarget: Playlist? = null
|
||||
private var lastUpdateTime = -1L
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -512,11 +508,5 @@ class HomeFragment :
|
|||
private companion object {
|
||||
val VP_RECYCLER_FIELD: Field by lazyReflectedField(ViewPager2::class, "mRecyclerView")
|
||||
val RV_TOUCH_SLOP_FIELD: Field by lazyReflectedField(RecyclerView::class, "mTouchSlop")
|
||||
val FAB_HIDE_FROM_USER_FIELD: Method by
|
||||
lazyReflectedMethod(
|
||||
FloatingActionButton::class,
|
||||
"hide",
|
||||
FloatingActionButton.OnVisibilityChangedListener::class,
|
||||
Boolean::class)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import org.oxycblt.auxio.BuildConfig
|
|||
import org.oxycblt.auxio.image.covers.SettingCovers
|
||||
import org.oxycblt.musikr.covers.CoverResult
|
||||
|
||||
class CoverProvider() : ContentProvider() {
|
||||
class CoverProvider : ContentProvider() {
|
||||
override fun onCreate(): Boolean = true
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||
|
|
|
@ -37,6 +37,7 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.annotation.Px
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isEmpty
|
||||
import androidx.core.view.updateMarginsRelative
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import coil3.ImageLoader
|
||||
|
@ -172,7 +173,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
super.onFinishInflate()
|
||||
|
||||
// The image isn't added if other children have populated the body. This is by design.
|
||||
if (childCount == 0) {
|
||||
if (isEmpty()) {
|
||||
addView(image)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,9 +19,9 @@
|
|||
package org.oxycblt.auxio.image.coil
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
|
@ -90,8 +90,7 @@ private constructor(
|
|||
val mosaicFrameSize =
|
||||
Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2))
|
||||
|
||||
val mosaicBitmap =
|
||||
Bitmap.createBitmap(mosaicSize.width, mosaicSize.height, Bitmap.Config.ARGB_8888)
|
||||
val mosaicBitmap = createBitmap(mosaicSize.width, mosaicSize.height)
|
||||
val canvas = Canvas(mosaicBitmap)
|
||||
|
||||
var x = 0
|
||||
|
|
|
@ -18,32 +18,20 @@
|
|||
|
||||
package org.oxycblt.auxio.image.coil
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.decode.DataSource
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.FetchResult
|
||||
import coil3.fetch.Fetcher
|
||||
import coil3.fetch.ImageFetchResult
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import coil3.size.Dimension
|
||||
import coil3.size.Size
|
||||
import coil3.size.pxOrElse
|
||||
import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
import okio.FileSystem
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.oxycblt.musikr.covers.Cover
|
||||
|
||||
class CoverFetcher private constructor(private val context: Context, private val cover: Cover) :
|
||||
Fetcher {
|
||||
class CoverFetcher private constructor(private val cover: Cover) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
val stream = cover.open() ?: return null
|
||||
return SourceFetchResult(
|
||||
|
@ -52,59 +40,8 @@ class CoverFetcher private constructor(private val context: Context, private val
|
|||
dataSource = DataSource.DISK)
|
||||
}
|
||||
|
||||
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
|
||||
private suspend fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {
|
||||
// Use whatever size coil gives us to create the mosaic.
|
||||
val mosaicSize = android.util.Size(size.width.mosaicSize(), size.height.mosaicSize())
|
||||
val mosaicFrameSize =
|
||||
Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2))
|
||||
|
||||
val mosaicBitmap =
|
||||
Bitmap.createBitmap(mosaicSize.width, mosaicSize.height, 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 == mosaicSize.height) {
|
||||
break
|
||||
}
|
||||
|
||||
// Crop the bitmap down to a square so it leaves no empty space
|
||||
// TODO: Work around this
|
||||
val bitmap =
|
||||
SquareCropTransformation.INSTANCE.transform(
|
||||
BitmapFactory.decodeStream(stream), mosaicFrameSize)
|
||||
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
||||
|
||||
x += bitmap.width
|
||||
if (x == mosaicSize.width) {
|
||||
x = 0
|
||||
y += bitmap.height
|
||||
}
|
||||
}
|
||||
|
||||
// It's way easier to map this into a drawable then try to serialize it into an
|
||||
// BufferedSource. Just make sure we mark it as "sampled" so Coil doesn't try to
|
||||
// load low-res mosaics into high-res ImageViews.
|
||||
return ImageFetchResult(
|
||||
image = mosaicBitmap.toDrawable(context.resources).asImage(),
|
||||
isSampled = true,
|
||||
dataSource = DataSource.DISK)
|
||||
}
|
||||
|
||||
private fun Dimension.mosaicSize(): Int {
|
||||
// Since we want the mosaic to be perfectly divisible into two, we need to round any
|
||||
// odd image sizes upwards to prevent the mosaic creation from failing.
|
||||
val size = pxOrElse { 512 }
|
||||
return if (size.mod(2) > 0) size + 1 else size
|
||||
}
|
||||
|
||||
class Factory @Inject constructor() : Fetcher.Factory<Cover> {
|
||||
override fun create(data: Cover, options: Options, imageLoader: ImageLoader) =
|
||||
CoverFetcher(options.context, data)
|
||||
CoverFetcher(data)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,8 +38,8 @@ import coil3.transform.Transformation
|
|||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* A vendoring of [coil.transform.RoundedCornersTransformation] that can handle non-1:1 aspect ratio
|
||||
* images without cropping them.
|
||||
* A vendoring of coil's RoundedCornersTransformation that can handle non-1:1 aspect ratio images
|
||||
* without cropping them.
|
||||
*
|
||||
* @author Coil Team, Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
package org.oxycblt.auxio.image.coil
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.core.graphics.scale
|
||||
import coil3.size.Size
|
||||
import coil3.size.pxOrElse
|
||||
import coil3.transform.Transformation
|
||||
|
@ -46,7 +47,7 @@ class SquareCropTransformation : Transformation() {
|
|||
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.scale(desiredWidth, desiredHeight)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import android.view.ViewGroup
|
|||
import android.view.WindowInsets
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.core.view.isEmpty
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.updatePaddingRelative
|
||||
import androidx.core.widget.TextViewCompat
|
||||
|
@ -91,7 +92,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
private var thumbAnimator: Animator? = null
|
||||
|
||||
private val thumbView =
|
||||
context.inflater.inflate(R.layout.view_scroll_thumb, null).apply {
|
||||
context.inflater.inflate(R.layout.view_scroll_thumb, this).apply {
|
||||
thumbSlider.jumpOut(this)
|
||||
}
|
||||
private val thumbPadding = Rect(0, 0, 0, 0)
|
||||
|
@ -339,7 +340,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
// [proportion of scroll position to scroll range] * [total thumb range]
|
||||
// This is somewhat adapted from the androidx RecyclerView FastScroller implementation.
|
||||
val offsetY = computeVerticalScrollOffset()
|
||||
if (computeVerticalScrollRange() < height || childCount == 0) {
|
||||
if (computeVerticalScrollRange() < height || isEmpty()) {
|
||||
fastScrollingPossible = false
|
||||
hideThumb()
|
||||
hidePopup()
|
||||
|
|
|
@ -188,8 +188,8 @@ interface MusicRepository {
|
|||
/**
|
||||
* Flags indicating which kinds of music information changed.
|
||||
*
|
||||
* @param deviceLibrary Whether the current [DeviceLibrary] has changed.
|
||||
* @param library Whether the current [Playlist]s have changed.
|
||||
* @param deviceLibrary Whether the current songs/albums/artists/genres has changed.
|
||||
* @param userLibrary Whether the current playlists have changed.
|
||||
*/
|
||||
data class Changes(val deviceLibrary: Boolean, val userLibrary: Boolean)
|
||||
|
||||
|
@ -244,7 +244,7 @@ constructor(
|
|||
) : MusicRepository {
|
||||
private val updateListeners = mutableListOf<MusicRepository.UpdateListener>()
|
||||
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
|
||||
@Volatile private var indexingWorker: MusicRepository.IndexingWorker? = null
|
||||
@Volatile private var indexingWorker: IndexingWorker? = null
|
||||
|
||||
@Volatile override var library: MutableLibrary? = null
|
||||
@Volatile private var previousCompletedState: IndexingState.Completed? = null
|
||||
|
@ -283,7 +283,7 @@ constructor(
|
|||
}
|
||||
|
||||
@Synchronized
|
||||
override fun registerWorker(worker: MusicRepository.IndexingWorker) {
|
||||
override fun registerWorker(worker: IndexingWorker) {
|
||||
if (indexingWorker != null) {
|
||||
L.w("Worker is already registered")
|
||||
return
|
||||
|
@ -293,7 +293,7 @@ constructor(
|
|||
}
|
||||
|
||||
@Synchronized
|
||||
override fun unregisterWorker(worker: MusicRepository.IndexingWorker) {
|
||||
override fun unregisterWorker(worker: IndexingWorker) {
|
||||
if (indexingWorker !== worker) {
|
||||
L.w("Given worker did not match current worker")
|
||||
return
|
||||
|
|
|
@ -27,15 +27,10 @@ import org.oxycblt.auxio.R
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
enum class MusicType {
|
||||
/** @see Song */
|
||||
SONGS,
|
||||
/** @see Album */
|
||||
ALBUMS,
|
||||
/** @see Artist */
|
||||
ARTISTS,
|
||||
/** @see Genre */
|
||||
GENRES,
|
||||
/** @see Playlist */
|
||||
PLAYLISTS;
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,6 +25,7 @@ import android.view.LayoutInflater
|
|||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.net.toUri
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
@ -80,7 +81,7 @@ class MusicSourcesDialog :
|
|||
|
||||
val locations =
|
||||
savedInstanceState?.getStringArrayList(KEY_PENDING_LOCATIONS)?.mapNotNull {
|
||||
MusicLocation.existing(requireContext(), Uri.parse(it))
|
||||
MusicLocation.existing(requireContext(), it.toUri())
|
||||
} ?: musicSettings.musicLocations
|
||||
|
||||
locationAdapter.addAll(locations)
|
||||
|
|
|
@ -51,7 +51,7 @@ class NewLocationFooterAdapter(private val listener: Listener) :
|
|||
}
|
||||
|
||||
/**
|
||||
* A [RecyclerView.ViewHolder] that displays a "New Playlist" choice in [NewPlaylistFooterAdapter].
|
||||
* A [RecyclerView.ViewHolder] that displays a "New Playlist" choice in [NewLocationFooterAdapter].
|
||||
* Use [from] to create an instance.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
|
|
|
@ -48,13 +48,6 @@ fun Long.dsToMs() = times(100)
|
|||
*/
|
||||
fun Long.dsToSecs() = floorDiv(10)
|
||||
|
||||
/**
|
||||
* Convert seconds into milliseconds.
|
||||
*
|
||||
* @return A converted millisecond value.
|
||||
*/
|
||||
fun Long.secsToMs() = times(1000)
|
||||
|
||||
/**
|
||||
* Convert a millisecond value into a string duration.
|
||||
*
|
||||
|
|
|
@ -18,7 +18,9 @@
|
|||
|
||||
package org.oxycblt.auxio.playback.service
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.source.ShuffleOrder
|
||||
|
||||
/**
|
||||
|
@ -28,6 +30,7 @@ import androidx.media3.exoplayer.source.ShuffleOrder
|
|||
*
|
||||
* @author media3 team, Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
@OptIn(UnstableApi::class)
|
||||
class BetterShuffleOrder(private val shuffled: IntArray) : ShuffleOrder {
|
||||
private val indexInShuffled: IntArray = IntArray(shuffled.size)
|
||||
|
||||
|
|
|
@ -22,11 +22,13 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.media.audiofx.AudioEffect
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.RenderersFactory
|
||||
|
@ -62,6 +64,7 @@ import org.oxycblt.musikr.MusicParent
|
|||
import org.oxycblt.musikr.Song
|
||||
import timber.log.Timber as L
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class ExoPlaybackStateHolder(
|
||||
private val context: Context,
|
||||
private val player: ExoPlayer,
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
package org.oxycblt.auxio.playback.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.ContentDataSource
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
|
@ -41,6 +43,7 @@ import dagger.hilt.components.SingletonComponent
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@OptIn(UnstableApi::class)
|
||||
class SystemModule {
|
||||
@Provides
|
||||
fun mediaSourceFactory(
|
||||
|
|
|
@ -20,9 +20,9 @@ package org.oxycblt.auxio.settings
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
|
@ -102,7 +102,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
|||
}
|
||||
|
||||
private fun Context.sendEmail(recipient: String) {
|
||||
val intent = Intent(Intent.ACTION_SENDTO).apply { data = Uri.parse("mailto:$recipient") }
|
||||
val intent = Intent(Intent.ACTION_SENDTO).apply { data = "mailto:$recipient".toUri() }
|
||||
startIntent(intent)
|
||||
}
|
||||
|
||||
|
|
|
@ -46,25 +46,25 @@ class AnimConfig(
|
|||
|
||||
companion object {
|
||||
val STANDARD = MR.attr.motionEasingStandardInterpolator
|
||||
val EMPHASIZED = MR.attr.motionEasingEmphasizedInterpolator
|
||||
// val EMPHASIZED = MR.attr.motionEasingEmphasizedInterpolator
|
||||
val EMPHASIZED_ACCELERATE = MR.attr.motionEasingEmphasizedAccelerateInterpolator
|
||||
val EMPHASIZED_DECELERATE = MR.attr.motionEasingEmphasizedDecelerateInterpolator
|
||||
val SHORT1 = MR.attr.motionDurationShort1 to 50
|
||||
val SHORT2 = MR.attr.motionDurationShort2 to 100
|
||||
// val SHORT2 = MR.attr.motionDurationShort2 to 100
|
||||
val SHORT3 = MR.attr.motionDurationShort3 to 150
|
||||
val SHORT4 = MR.attr.motionDurationShort4 to 200
|
||||
// val SHORT4 = MR.attr.motionDurationShort4 to 200
|
||||
val MEDIUM1 = MR.attr.motionDurationMedium1 to 250
|
||||
val MEDIUM2 = MR.attr.motionDurationMedium2 to 300
|
||||
val MEDIUM3 = MR.attr.motionDurationMedium3 to 350
|
||||
val MEDIUM4 = MR.attr.motionDurationMedium4 to 400
|
||||
val LONG1 = MR.attr.motionDurationLong1 to 450
|
||||
val LONG2 = MR.attr.motionDurationLong2 to 500
|
||||
val LONG3 = MR.attr.motionDurationLong3 to 550
|
||||
val LONG4 = MR.attr.motionDurationLong4 to 600
|
||||
val EXTRA_LONG1 = MR.attr.motionDurationExtraLong1 to 700
|
||||
val EXTRA_LONG2 = MR.attr.motionDurationExtraLong2 to 800
|
||||
val EXTRA_LONG3 = MR.attr.motionDurationExtraLong3 to 900
|
||||
val EXTRA_LONG4 = MR.attr.motionDurationExtraLong4 to 1000
|
||||
// val MEDIUM4 = MR.attr.motionDurationMedium4 to 400
|
||||
// val LONG1 = MR.attr.motionDurationLong1 to 450
|
||||
// val LONG2 = MR.attr.motionDurationLong2 to 500
|
||||
// val LONG3 = MR.attr.motionDurationLong3 to 550
|
||||
// val LONG4 = MR.attr.motionDurationLong4 to 600
|
||||
// val EXTRA_LONG1 = MR.attr.motionDurationExtraLong1 to 700
|
||||
// val EXTRA_LONG2 = MR.attr.motionDurationExtraLong2 to 800
|
||||
// val EXTRA_LONG3 = MR.attr.motionDurationExtraLong3 to 900
|
||||
// val EXTRA_LONG4 = MR.attr.motionDurationExtraLong4 to 1000
|
||||
|
||||
fun of(context: Context, @AttrRes interpolator: Int, duration: Pair<Int, Int>) =
|
||||
AnimConfig(context, interpolator, duration.first, duration.second)
|
||||
|
@ -122,7 +122,7 @@ private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun jumpToFadeIn(view: View) {
|
||||
private fun jumpToFadeIn(view: View) {
|
||||
view.apply {
|
||||
alpha = 1f
|
||||
scaleX = 1.0f
|
||||
|
|
|
@ -24,7 +24,6 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.PointF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
|
@ -36,7 +35,6 @@ import androidx.appcompat.widget.Toolbar
|
|||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.children
|
||||
import androidx.navigation.NavController
|
||||
|
@ -106,10 +104,6 @@ private fun isUnderImpl(
|
|||
val View.isRtl: Boolean
|
||||
get() = layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
|
||||
/** Whether this [Drawable] is using an RTL layout direction. */
|
||||
val Drawable.isRtl: Boolean
|
||||
get() = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL
|
||||
|
||||
/** Get a [Context] from a [ViewBinding]'s root [View]. */
|
||||
val ViewBinding.context: Context
|
||||
get() = root.context
|
||||
|
@ -357,7 +351,7 @@ fun Context.startIntent(intent: Intent) {
|
|||
// No app installed to open the link
|
||||
showToast(R.string.err_no_app)
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
} else {
|
||||
// On older versions of android, opening links from an ACTION_VIEW intent might
|
||||
// not work in all cases, especially when no default app was set. If that is the
|
||||
// case, we will try to manually handle these cases before we try to launch the
|
||||
|
|
|
@ -22,18 +22,11 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import java.util.concurrent.TimeoutException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import timber.log.Timber as L
|
||||
|
||||
/**
|
||||
* A wrapper around [StateFlow] exposing a one-time consumable event.
|
||||
|
@ -153,71 +146,3 @@ private fun Fragment.launch(
|
|||
) {
|
||||
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) }
|
||||
}
|
||||
|
||||
const val DEFAULT_TIMEOUT = 60000L
|
||||
|
||||
/**
|
||||
* Wraps [SendChannel.send] with a specified timeout.
|
||||
*
|
||||
* @param element The element to send.
|
||||
* @param timeout The timeout in milliseconds. Defaults to 10 seconds.
|
||||
* @throws TimeoutException If the timeout is reached, provides context on what element
|
||||
* specifically.
|
||||
*/
|
||||
suspend fun <E> SendChannel<E>.sendWithTimeout(element: E, timeout: Long = DEFAULT_TIMEOUT) {
|
||||
try {
|
||||
withTimeout(timeout) { send(element) }
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
L.e("Failed to send element to channel $e in ${timeout}ms.")
|
||||
if (BuildConfig.DEBUG) {
|
||||
throw TimeoutException("Timed out sending element to channel: $e")
|
||||
} else {
|
||||
L.e(e.stackTraceToString())
|
||||
send(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a [ReceiveChannel] consumption with a specified timeout. Note that the timeout will only
|
||||
* start on the first element received, as to prevent initialization of dependent coroutines being
|
||||
* interpreted as a timeout.
|
||||
*
|
||||
* @param action The action to perform on each element received.
|
||||
* @param timeout The timeout in milliseconds. Defaults to 10 seconds.
|
||||
* @throws TimeoutException If the timeout is reached, provides context on what element
|
||||
* specifically.
|
||||
*/
|
||||
suspend fun <E> ReceiveChannel<E>.forEachWithTimeout(
|
||||
timeout: Long = DEFAULT_TIMEOUT,
|
||||
action: suspend (E) -> Unit
|
||||
) {
|
||||
var exhausted = false
|
||||
var subsequent = false
|
||||
val handler: suspend () -> Unit = {
|
||||
val value = receiveCatching()
|
||||
if (value.isClosed && value.exceptionOrNull() == null) {
|
||||
exhausted = true
|
||||
} else {
|
||||
action(value.getOrThrow())
|
||||
}
|
||||
}
|
||||
while (!exhausted) {
|
||||
try {
|
||||
if (subsequent) {
|
||||
withTimeout(timeout) { handler() }
|
||||
} else {
|
||||
handler()
|
||||
subsequent = true
|
||||
}
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
L.e("Failed to send element to channel $e in ${timeout}ms.")
|
||||
if (BuildConfig.DEBUG) {
|
||||
throw TimeoutException("Timed out sending element to channel: $e")
|
||||
} else {
|
||||
L.e(e.stackTraceToString())
|
||||
handler()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.widgets
|
|||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import androidx.core.graphics.scale
|
||||
import coil3.size.Size
|
||||
import coil3.transform.Transformation
|
||||
import kotlin.math.sqrt
|
||||
|
@ -49,7 +50,7 @@ class WidgetBitmapTransformation(reduce: Float) : Transformation() {
|
|||
val scale = sqrt(maxBitmapArea / inputArea.toDouble())
|
||||
val newWidth = (input.width * scale).toInt()
|
||||
val newHeight = (input.height * scale).toInt()
|
||||
return Bitmap.createScaledBitmap(input, newWidth, newHeight, true)
|
||||
return input.scale(newWidth, newHeight)
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
package org.oxycblt.auxio.widgets
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProviderInfo
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
@ -66,11 +65,6 @@ fun RemoteViews.setLayoutDirection(@IdRes viewId: Int, layoutDirection: Int) {
|
|||
setInt(viewId, "setLayoutDirection", layoutDirection)
|
||||
}
|
||||
|
||||
fun AppWidgetManager.setWidgetPreviewCompat(component: ComponentName, remoteViews: RemoteViews) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
setWidgetPreview(component, AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN, remoteViews)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update the app widget layouts corresponding to the given [WidgetProvider] [ComponentName] with an
|
||||
* adaptive layout, in a version-compatible manner.
|
||||
|
|
|
@ -119,7 +119,8 @@
|
|||
app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/detail_cover" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/detail_cover"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
|
||||
<org.oxycblt.auxio.ui.RippleFixMaterialButton
|
||||
android:id="@+id/detail_shuffle_button"
|
||||
|
|
|
@ -101,7 +101,8 @@
|
|||
app:icon="@drawable/ic_play_24"
|
||||
app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/detail_info" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/detail_info"
|
||||
tools:ignore="RtlSymmetry"/>
|
||||
|
||||
<org.oxycblt.auxio.ui.RippleFixMaterialButton
|
||||
android:id="@+id/detail_shuffle_button"
|
||||
|
|
|
@ -129,7 +129,8 @@
|
|||
app:layout_constraintBottom_toBottomOf="@+id/detail_play_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/detail_play_button"
|
||||
app:layout_constraintTop_toTopOf="@+id/detail_play_button" />
|
||||
app:layout_constraintTop_toTopOf="@+id/detail_play_button"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
|
|
|
@ -132,7 +132,8 @@
|
|||
app:layout_constraintBottom_toBottomOf="@+id/detail_play_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/detail_play_button"
|
||||
app:layout_constraintTop_toTopOf="@+id/detail_play_button" />
|
||||
app:layout_constraintTop_toTopOf="@+id/detail_play_button"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
android:layout_height="@dimen/size_icon_huge"
|
||||
android:layout_marginBottom="@dimen/spacing_small"
|
||||
android:src="@drawable/ic_song_48"
|
||||
tools:ignore="ContentDescription"
|
||||
app:tint="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
|
|
|
@ -3,12 +3,13 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/spacing_tiny"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/size_touchable_small"
|
||||
android:scaleType="centerInside"
|
||||
tools:ignore="ContentDescription"
|
||||
android:src="@drawable/ui_scroll_thumb" />
|
||||
|
||||
</FrameLayout>
|
|
@ -22,7 +22,7 @@
|
|||
android:scaleType="centerCrop"
|
||||
android:background="@drawable/ui_widget_bg_round"
|
||||
android:clipToOutline="true"
|
||||
tools:ignore="ContentDescription" />
|
||||
tools:ignore="ContentDescription,UnusedAttribute" />
|
||||
|
||||
<android.widget.LinearLayout
|
||||
android:id="@+id/widget_panel"
|
||||
|
|
|
@ -20,8 +20,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@drawable/ui_widget_bg_round"
|
||||
android:clipToOutline="true"
|
||||
tools:ignore="ContentDescription" />
|
||||
tools:ignore="ContentDescription,UnusedAttribute" />
|
||||
|
||||
<android.widget.LinearLayout
|
||||
android:id="@+id/widget_panel"
|
||||
|
|
|
@ -302,7 +302,7 @@
|
|||
<string name="clr_brown">Kafe</string>
|
||||
<string name="fmt_db_neg">-%.1f dB</string>
|
||||
<string name="fmt_bitrate">%d kbps</string>
|
||||
<string name="fmt_indexing">Duke ngarkuar bibliotekën tuaj muzikore... (%1$d/%2$d)</string>
|
||||
<string name="fmt_indexing">Duke ngarkuar bibliotekën tuaj muzikore… (%1$d/%2$d)</string>
|
||||
<string name="fmt_lib_song_count">Këngët e ngarkuara: %d</string>
|
||||
<string name="fmt_lib_album_count">Albumet e ngarkuara: %d</string>
|
||||
<string name="fmt_lib_artist_count">Artistët e ngarkuar: %d</string>
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<style name="Theme.Auxio.Black" parent="Theme.Auxio.Base">
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--
|
||||
Can't meaningfully dim the default black theme colors, have to tweak the palette manually
|
||||
-->
|
||||
<style name="Theme.Auxio.Black" parent="Theme.Auxio.Base" tools:ignore="PrivateResource">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<item name="colorSurfaceDim">@color/m3_ref_palette_dynamic_neutral_variant4</item>
|
||||
<item name="colorSurfaceBright">@color/m3_ref_palette_dynamic_neutral_variant12</item>
|
||||
|
|
|
@ -59,8 +59,7 @@ internal interface CacheReadDao {
|
|||
|
||||
@Dao
|
||||
internal interface CacheWriteDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun updateSong(CachedSongData: CachedSongData)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(data: CachedSongData)
|
||||
|
||||
@Transaction
|
||||
suspend fun deleteExcludingUris(uris: Set<String>) {
|
||||
|
|
|
@ -37,6 +37,10 @@ sealed interface Name : Comparable<Name> {
|
|||
/** A tokenized version of the name that will be compared. */
|
||||
abstract val tokens: List<Token>
|
||||
|
||||
abstract override fun hashCode(): Int
|
||||
|
||||
abstract override fun equals(other: Any?): Boolean
|
||||
|
||||
final override fun compareTo(other: Name) =
|
||||
when (other) {
|
||||
is Known -> {
|
||||
|
|
Loading…
Reference in a new issue