home: make playlist add a speed dial
Add a speed dial menu that allows you to create a new playlist or import a playlist from elsewhere.
This commit is contained in:
parent
d3de34ed5e
commit
08ca71b7b0
9 changed files with 427 additions and 144 deletions
|
@ -147,6 +147,9 @@ dependencies {
|
|||
// Logging
|
||||
implementation 'com.jakewharton.timber:timber:5.0.1'
|
||||
|
||||
// Speed dial
|
||||
implementation "com.leinardi.android:speed-dial:3.3.0"
|
||||
|
||||
// Testing
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* FlipFloatingActionButton.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.home
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.google.android.material.R
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* An extension of [FloatingActionButton] that enables the ability to fade in and out between
|
||||
* several states, as in the Material Design 3 specification.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class FlipFloatingActionButton
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.floatingActionButtonStyle
|
||||
) : FloatingActionButton(context, attrs, defStyleAttr) {
|
||||
private var pendingConfig: PendingConfig? = null
|
||||
private var flipping = false
|
||||
|
||||
override fun show() {
|
||||
// Will already show eventually, need to do nothing.
|
||||
if (flipping) {
|
||||
logD("Already flipping, aborting show")
|
||||
return
|
||||
}
|
||||
// Apply the new configuration possibly set in flipTo. This should occur even if
|
||||
// a flip was canceled by a hide.
|
||||
pendingConfig?.run {
|
||||
logD("Applying pending configuration")
|
||||
setImageResource(iconRes)
|
||||
contentDescription = context.getString(contentDescriptionRes)
|
||||
setOnClickListener(clickListener)
|
||||
}
|
||||
pendingConfig = null
|
||||
logD("Beginning show")
|
||||
super.show()
|
||||
}
|
||||
|
||||
override fun hide() {
|
||||
if (flipping) {
|
||||
logD("Hide was called, aborting flip")
|
||||
}
|
||||
// Not flipping anymore, disable the flag so that the FAB is not re-shown.
|
||||
flipping = false
|
||||
// Don't pass any kind of listener so that future flip operations will not be able
|
||||
// to show the FAB again.
|
||||
logD("Beginning hide")
|
||||
super.hide()
|
||||
}
|
||||
|
||||
/**
|
||||
* Flip to a new FAB state.
|
||||
*
|
||||
* @param iconRes The resource of the new FAB icon.
|
||||
* @param contentDescriptionRes The resource of the new FAB content description.
|
||||
*/
|
||||
fun flipTo(
|
||||
@DrawableRes iconRes: Int,
|
||||
@StringRes contentDescriptionRes: Int,
|
||||
clickListener: OnClickListener
|
||||
) {
|
||||
// Avoid doing a flip if the given config is already being applied.
|
||||
if (tag == iconRes) return
|
||||
tag = iconRes
|
||||
pendingConfig = PendingConfig(iconRes, contentDescriptionRes, clickListener)
|
||||
|
||||
// Already hiding for whatever reason, apply the configuration when the FAB is shown again.
|
||||
if (!isOrWillBeHidden) {
|
||||
logD("Starting hide for flip")
|
||||
flipping = true
|
||||
// We will re-show the FAB later, assuming that there was not a prior flip operation.
|
||||
super.hide(FlipVisibilityListener())
|
||||
} else {
|
||||
logD("Already hiding, will apply config later")
|
||||
}
|
||||
}
|
||||
|
||||
private data class PendingConfig(
|
||||
@DrawableRes val iconRes: Int,
|
||||
@StringRes val contentDescriptionRes: Int,
|
||||
val clickListener: OnClickListener
|
||||
)
|
||||
|
||||
private inner class FlipVisibilityListener : OnVisibilityChangedListener() {
|
||||
override fun onHidden(fab: FloatingActionButton) {
|
||||
if (!flipping) return
|
||||
logD("Starting show for flip")
|
||||
flipping = false
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -36,10 +36,12 @@ 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.BuildConfig
|
||||
import org.oxycblt.auxio.R
|
||||
|
@ -71,6 +73,7 @@ import org.oxycblt.auxio.util.collect
|
|||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.getColorCompat
|
||||
import org.oxycblt.auxio.util.lazyReflectedField
|
||||
import org.oxycblt.auxio.util.lazyReflectedMethod
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
import org.oxycblt.auxio.util.navigateSafe
|
||||
|
@ -166,6 +169,35 @@ class HomeFragment :
|
|||
// re-creating the ViewPager.
|
||||
setupPager(binding)
|
||||
|
||||
binding.homeShuffleFab.setOnClickListener {
|
||||
logD("Shuffling")
|
||||
playbackModel.shuffleAll()
|
||||
}
|
||||
|
||||
binding.homeNewPlaylistFab.apply {
|
||||
inflate(R.menu.new_playlist_actions)
|
||||
setOnActionSelectedListener { action ->
|
||||
when (action.id) {
|
||||
R.id.action_new_playlist -> {
|
||||
logD("Creating playlist")
|
||||
musicModel.createPlaylist()
|
||||
}
|
||||
R.id.action_import_playlist -> {
|
||||
TODO("Not implemented")
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
close()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
hideAllFabs()
|
||||
updateFabVisibility(
|
||||
homeModel.songList.value,
|
||||
homeModel.isFastScrolling.value,
|
||||
homeModel.currentTabType.value)
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
collect(homeModel.recreateTabs.flow, ::handleRecreate)
|
||||
collectImmediately(homeModel.currentTabType, ::updateCurrentTab)
|
||||
|
@ -291,17 +323,7 @@ class HomeFragment :
|
|||
MusicType.PLAYLISTS -> R.id.home_playlist_recycler
|
||||
}
|
||||
|
||||
if (tabType != MusicType.PLAYLISTS) {
|
||||
logD("Flipping to shuffle button")
|
||||
binding.homeFab.flipTo(R.drawable.ic_shuffle_off_24, R.string.desc_shuffle_all) {
|
||||
playbackModel.shuffleAll()
|
||||
}
|
||||
} else {
|
||||
logD("Flipping to playlist button")
|
||||
binding.homeFab.flipTo(R.drawable.ic_add_24, R.string.desc_new_playlist) {
|
||||
musicModel.createPlaylist()
|
||||
}
|
||||
}
|
||||
updateFabVisibility(homeModel.songList.value, homeModel.isFastScrolling.value, tabType)
|
||||
}
|
||||
|
||||
private fun handleRecreate(recreate: Unit?) {
|
||||
|
@ -333,7 +355,10 @@ class HomeFragment :
|
|||
private fun setupCompleteState(binding: FragmentHomeBinding, error: Exception?) {
|
||||
if (error == null) {
|
||||
logD("Received ok response")
|
||||
binding.homeFab.show()
|
||||
updateFabVisibility(
|
||||
homeModel.songList.value,
|
||||
homeModel.isFastScrolling.value,
|
||||
homeModel.currentTabType.value)
|
||||
binding.homeIndexingContainer.visibility = View.INVISIBLE
|
||||
return
|
||||
}
|
||||
|
@ -440,16 +465,75 @@ class HomeFragment :
|
|||
}
|
||||
|
||||
private fun updateFab(songs: List<Song>, isFastScrolling: Boolean) {
|
||||
updateFabVisibility(songs, isFastScrolling, homeModel.currentTabType.value)
|
||||
}
|
||||
|
||||
private fun updateFabVisibility(
|
||||
songs: List<Song>,
|
||||
isFastScrolling: Boolean,
|
||||
tabType: MusicType
|
||||
) {
|
||||
val binding = requireBinding()
|
||||
// If there are no songs, it's likely that the library has not been loaded, so
|
||||
// displaying the shuffle FAB makes no sense. We also don't want the fast scroll
|
||||
// popup to overlap with the FAB, so we hide the FAB when fast scrolling too.
|
||||
if (songs.isEmpty() || isFastScrolling) {
|
||||
logD("Hiding fab: [empty: ${songs.isEmpty()} scrolling: $isFastScrolling]")
|
||||
binding.homeFab.hide()
|
||||
hideAllFabs()
|
||||
} else {
|
||||
logD("Showing fab")
|
||||
binding.homeFab.show()
|
||||
if (tabType != MusicType.PLAYLISTS) {
|
||||
logD("Showing shuffle button")
|
||||
if (binding.homeShuffleFab.isOrWillBeShown) {
|
||||
logD("Nothing to do")
|
||||
return
|
||||
}
|
||||
|
||||
if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) {
|
||||
logD("Animating transition")
|
||||
binding.homeNewPlaylistFab.hide(
|
||||
object : FloatingActionButton.OnVisibilityChangedListener() {
|
||||
override fun onHidden(fab: FloatingActionButton) {
|
||||
super.onHidden(fab)
|
||||
binding.homeShuffleFab.show()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
logD("Showing immediately")
|
||||
binding.homeShuffleFab.show()
|
||||
}
|
||||
} else {
|
||||
logD("Showing playlist button")
|
||||
if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) {
|
||||
logD("Nothing to do")
|
||||
return
|
||||
}
|
||||
|
||||
logD(binding.homeShuffleFab.isOrWillBeShown)
|
||||
|
||||
if (binding.homeShuffleFab.isOrWillBeShown) {
|
||||
logD("Animating transition")
|
||||
binding.homeShuffleFab.hide(
|
||||
object : FloatingActionButton.OnVisibilityChangedListener() {
|
||||
override fun onHidden(fab: FloatingActionButton) {
|
||||
super.onHidden(fab)
|
||||
binding.homeNewPlaylistFab.show()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
logD("Showing immediately")
|
||||
binding.homeNewPlaylistFab.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideAllFabs() {
|
||||
val binding = requireBinding()
|
||||
if (binding.homeShuffleFab.isOrWillBeShown) {
|
||||
FAB_HIDE_FROM_USER_FIELD.invoke(binding.homeShuffleFab, null, false)
|
||||
}
|
||||
if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) {
|
||||
FAB_HIDE_FROM_USER_FIELD.invoke(binding.homeNewPlaylistFab.mainFab, null, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -568,6 +652,12 @@ 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)
|
||||
const val KEY_LAST_TRANSITION_ID = BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS"
|
||||
}
|
||||
}
|
||||
|
|
256
app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt
Normal file
256
app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt
Normal file
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* Copyright (c) 2018 Auxio Project
|
||||
* ThemedSpeedDialView.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.home
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.RotateDrawable
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.util.Property
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.core.view.setMargins
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import com.leinardi.android.speeddial.FabWithLabelView
|
||||
import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||
import com.leinardi.android.speeddial.SpeedDialView
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||
import org.oxycblt.auxio.util.getDimenPixels
|
||||
|
||||
/**
|
||||
* Customized Speed Dial view with some bug fixes and Material 3 theming.
|
||||
*
|
||||
* Adapted from Material Files:
|
||||
* https://github.com/zhanghai/MaterialFiles/tree/79f1727cec72a6a089eb495f79193f87459fc5e3
|
||||
*
|
||||
* MODIFICATIONS:
|
||||
* - Removed dynamic theme changes based on the MaterialFile's Material 3 setting
|
||||
* - Adapted code to the extensions in this project
|
||||
*
|
||||
* @author Hai Zhang, Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class ThemedSpeedDialView : SpeedDialView {
|
||||
private var mainFabAnimator: Animator? = null
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
@AttrRes defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr)
|
||||
|
||||
init {
|
||||
// Work around ripple bug on Android 12 when useCompatPadding = true.
|
||||
// @see https://github.com/material-components/material-components-android/issues/2617
|
||||
mainFab.apply {
|
||||
updateLayoutParams<MarginLayoutParams> {
|
||||
setMargins(context.getDimenPixels(R.dimen.spacing_medium))
|
||||
}
|
||||
useCompatPadding = false
|
||||
}
|
||||
val context = context
|
||||
mainFabClosedBackgroundColor =
|
||||
context
|
||||
.getAttrColorCompat(com.google.android.material.R.attr.colorSecondaryContainer)
|
||||
.defaultColor
|
||||
mainFabClosedIconColor =
|
||||
context
|
||||
.getAttrColorCompat(com.google.android.material.R.attr.colorOnSecondaryContainer)
|
||||
.defaultColor
|
||||
mainFabOpenedBackgroundColor =
|
||||
context.getAttrColorCompat(androidx.appcompat.R.attr.colorPrimary).defaultColor
|
||||
mainFabOpenedIconColor =
|
||||
context
|
||||
.getAttrColorCompat(com.google.android.material.R.attr.colorOnPrimary)
|
||||
.defaultColor
|
||||
|
||||
// Always use our own animation to fix the library issue that ripple is rotated as well.
|
||||
val mainFabDrawable =
|
||||
RotateDrawable().apply {
|
||||
drawable = mainFab.drawable
|
||||
toDegrees = mainFabAnimationRotateAngle
|
||||
}
|
||||
mainFabAnimationRotateAngle = 0f
|
||||
setMainFabClosedDrawable(mainFabDrawable)
|
||||
setOnChangeListener(
|
||||
object : OnChangeListener {
|
||||
override fun onMainActionSelected(): Boolean = false
|
||||
|
||||
override fun onToggleChanged(isOpen: Boolean) {
|
||||
mainFabAnimator?.cancel()
|
||||
mainFabAnimator =
|
||||
createMainFabAnimator(isOpen).apply {
|
||||
addListener(
|
||||
object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
mainFabAnimator = null
|
||||
}
|
||||
})
|
||||
start()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun createMainFabAnimator(isOpen: Boolean): Animator =
|
||||
AnimatorSet().apply {
|
||||
playTogether(
|
||||
ObjectAnimator.ofArgb(
|
||||
mainFab,
|
||||
VIEW_PROPERTY_BACKGROUND_TINT,
|
||||
if (isOpen) mainFabOpenedBackgroundColor else mainFabClosedBackgroundColor),
|
||||
ObjectAnimator.ofArgb(
|
||||
mainFab,
|
||||
IMAGE_VIEW_PROPERTY_IMAGE_TINT,
|
||||
if (isOpen) mainFabOpenedIconColor else mainFabClosedIconColor),
|
||||
ObjectAnimator.ofInt(
|
||||
mainFab.drawable, DRAWABLE_PROPERTY_LEVEL, if (isOpen) 10000 else 0))
|
||||
duration = 200
|
||||
interpolator = FastOutSlowInInterpolator()
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
val overlayLayout = overlayLayout
|
||||
if (overlayLayout != null) {
|
||||
val surfaceColor =
|
||||
context.getAttrColorCompat(com.google.android.material.R.attr.colorSurface)
|
||||
val overlayColor = surfaceColor.defaultColor.withModulatedAlpha(0.87f)
|
||||
overlayLayout.setBackgroundColor(overlayColor)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.withModulatedAlpha(
|
||||
@FloatRange(from = 0.0, to = 1.0) alphaModulation: Float
|
||||
): Int {
|
||||
val alpha = (alpha * alphaModulation).roundToInt()
|
||||
return ((alpha shl 24) or (this and 0x00FFFFFF))
|
||||
}
|
||||
|
||||
override fun addActionItem(
|
||||
actionItem: SpeedDialActionItem,
|
||||
position: Int,
|
||||
animate: Boolean
|
||||
): FabWithLabelView? {
|
||||
val context = context
|
||||
val fabImageTintColor = context.getAttrColorCompat(androidx.appcompat.R.attr.colorPrimary)
|
||||
val fabBackgroundColor =
|
||||
context.getAttrColorCompat(com.google.android.material.R.attr.colorSurface)
|
||||
val labelColor = context.getAttrColorCompat(android.R.attr.textColorSecondary)
|
||||
val labelBackgroundColor = Color.TRANSPARENT
|
||||
val actionItem =
|
||||
SpeedDialActionItem.Builder(
|
||||
actionItem.id,
|
||||
// Should not be a resource, pass null to fail fast.
|
||||
actionItem.getFabImageDrawable(null))
|
||||
.setLabel(actionItem.getLabel(context))
|
||||
.setFabImageTintColor(fabImageTintColor.defaultColor)
|
||||
.setFabBackgroundColor(fabBackgroundColor.defaultColor)
|
||||
.setLabelColor(labelColor.defaultColor)
|
||||
.setLabelBackgroundColor(labelBackgroundColor)
|
||||
.setLabelClickable(actionItem.isLabelClickable)
|
||||
.setTheme(actionItem.theme)
|
||||
.create()
|
||||
return super.addActionItem(actionItem, position, animate)?.apply {
|
||||
fab.apply {
|
||||
updateLayoutParams<MarginLayoutParams> {
|
||||
val horizontalMargin = context.getDimenPixels(R.dimen.spacing_mid_large)
|
||||
setMargins(horizontalMargin, 0, horizontalMargin, 0)
|
||||
}
|
||||
useCompatPadding = false
|
||||
}
|
||||
|
||||
labelBackground.apply {
|
||||
useCompatPadding = false
|
||||
setContentPadding(0, 0, 0, 0)
|
||||
foreground = null
|
||||
(getChildAt(0) as TextView).apply {
|
||||
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_LabelLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val superState =
|
||||
BundleCompat.getParcelable(
|
||||
super.onSaveInstanceState() as Bundle, "superState", Parcelable::class.java)
|
||||
return State(superState, isOpen)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable) {
|
||||
state as State
|
||||
super.onRestoreInstanceState(state.superState)
|
||||
if (state.isOpen) {
|
||||
toggle(false)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val VIEW_PROPERTY_BACKGROUND_TINT =
|
||||
object : Property<View, Int>(Int::class.java, "backgroundTint") {
|
||||
override fun get(view: View): Int? = view.backgroundTintList!!.defaultColor
|
||||
|
||||
override fun set(view: View, value: Int?) {
|
||||
view.backgroundTintList = ColorStateList.valueOf(value!!)
|
||||
}
|
||||
}
|
||||
|
||||
private val IMAGE_VIEW_PROPERTY_IMAGE_TINT =
|
||||
object : Property<ImageView, Int>(Int::class.java, "imageTint") {
|
||||
override fun get(view: ImageView): Int? = view.imageTintList!!.defaultColor
|
||||
|
||||
override fun set(view: ImageView, value: Int?) {
|
||||
view.imageTintList = ColorStateList.valueOf(value!!)
|
||||
}
|
||||
}
|
||||
|
||||
private val DRAWABLE_PROPERTY_LEVEL =
|
||||
object : Property<Drawable, Int>(Int::class.java, "level") {
|
||||
override fun get(drawable: Drawable): Int? = drawable.level
|
||||
|
||||
override fun set(drawable: Drawable, value: Int?) {
|
||||
drawable.level = value!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize private class State(val superState: Parcelable?, val isOpen: Boolean) : Parcelable
|
||||
}
|
|
@ -435,7 +435,8 @@ constructor(
|
|||
// To prevent a deadlock, we want to close the channel with an exception
|
||||
// to cascade to and cancel all other routines before finally bubbling up
|
||||
// to the main extractor loop.
|
||||
incompleteSongs.close(e)
|
||||
logE("MediaStore extraction failed: $e")
|
||||
incompleteSongs.close(Exception("MediaStore extraction failed: e"))
|
||||
return@async
|
||||
}
|
||||
incompleteSongs.close()
|
||||
|
@ -449,8 +450,8 @@ constructor(
|
|||
try {
|
||||
tagExtractor.consume(incompleteSongs, completeSongs)
|
||||
} catch (e: Exception) {
|
||||
logD("Tag extraction failed: $e")
|
||||
completeSongs.close(e)
|
||||
logE("Tag extraction failed: $e")
|
||||
completeSongs.close(Exception("Tag extraction failed: $e"))
|
||||
return@async
|
||||
}
|
||||
completeSongs.close()
|
||||
|
@ -466,8 +467,8 @@ constructor(
|
|||
deviceLibraryFactory.create(
|
||||
completeSongs, processedSongs, separators, nameFactory)
|
||||
} catch (e: Exception) {
|
||||
logD("DeviceLibrary creation failed: $e")
|
||||
processedSongs.close(e)
|
||||
logE("DeviceLibrary creation failed: $e")
|
||||
processedSongs.close(Exception("DeviceLibrary creation failed: $e"))
|
||||
return@async Result.failure(e)
|
||||
}
|
||||
processedSongs.close()
|
||||
|
|
11
app/src/main/res/drawable/ic_import_24.xml
Normal file
11
app/src/main/res/drawable/ic_import_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L560,80L800,320L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM520,360L520,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800L720,800Q720,800 720,800Q720,800 720,800L720,360L520,360ZM240,160L240,160L240,360L240,360L240,160L240,360L240,360L240,800Q240,800 240,800Q240,800 240,800L240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160Z"/>
|
||||
</vector>
|
|
@ -8,6 +8,7 @@
|
|||
android:background="?attr/colorSurface"
|
||||
android:transitionGroup="true">
|
||||
|
||||
|
||||
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
|
||||
android:id="@+id/home_appbar"
|
||||
style="@style/Widget.Auxio.AppBarLayout">
|
||||
|
@ -147,18 +148,43 @@
|
|||
</FrameLayout>
|
||||
|
||||
<org.oxycblt.auxio.home.EdgeFrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
app:layout_anchor="@id/home_content"
|
||||
app:layout_anchorGravity="bottom|end">
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.oxycblt.auxio.home.FlipFloatingActionButton
|
||||
android:id="@+id/home_fab"
|
||||
style="@style/Widget.Auxio.FloatingActionButton.Adaptive"
|
||||
|
||||
<com.leinardi.android.speeddial.SpeedDialOverlayLayout
|
||||
android:id="@+id/home_speed_dial_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<org.oxycblt.auxio.home.ThemedSpeedDialView
|
||||
android:id="@+id/home_new_playlist_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/spacing_medium" />
|
||||
app:sdMainFabAnimationRotateAngle="135"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="bottom|end"
|
||||
app:sdMainFabClosedSrc="@drawable/ic_add_24"
|
||||
android:layout_gravity="bottom|end"
|
||||
app:sdMainFabClosedIconColor="@android:color/white"
|
||||
app:sdOverlayLayout="@+id/home_speed_dial_overlay" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/home_shuffle_fab"
|
||||
android:layout_margin="@dimen/spacing_medium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:src="@drawable/ic_shuffle_off_24" />
|
||||
|
||||
</org.oxycblt.auxio.home.EdgeFrameLayout>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
11
app/src/main/res/menu/new_playlist_actions.xml
Normal file
11
app/src/main/res/menu/new_playlist_actions.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/action_import_playlist"
|
||||
android:icon="@drawable/ic_import_24"
|
||||
android:title="@string/lbl_import_playlist" />
|
||||
<item
|
||||
android:id="@+id/action_new_playlist"
|
||||
android:icon="@drawable/ic_playlist_24"
|
||||
android:title="@string/lbl_empty_playlist" />
|
||||
</menu>
|
|
@ -88,6 +88,8 @@
|
|||
<string name="lbl_playlist">Playlist</string>
|
||||
<string name="lbl_playlists">Playlists</string>
|
||||
<string name="lbl_new_playlist">New playlist</string>
|
||||
<string name="lbl_empty_playlist">Empty playlist</string>
|
||||
<string name="lbl_import_playlist">Imported playlist</string>
|
||||
<string name="lbl_rename">Rename</string>
|
||||
<string name="lbl_rename_playlist">Rename playlist</string>
|
||||
<string name="lbl_delete">Delete</string>
|
||||
|
|
Loading…
Reference in a new issue