diff --git a/app/build.gradle b/app/build.gradle
index f414337b8..5d07581f0 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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"
diff --git a/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt b/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt
deleted file mode 100644
index c3cd4a82f..000000000
--- a/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt
+++ /dev/null
@@ -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 .
- */
-
-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()
- }
- }
-}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
index 01558611d..e49c08a45 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
@@ -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, isFastScrolling: Boolean) {
+ updateFabVisibility(songs, isFastScrolling, homeModel.currentTabType.value)
+ }
+
+ private fun updateFabVisibility(
+ songs: List,
+ 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"
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt
new file mode 100644
index 000000000..845bc617d
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt
@@ -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 .
+ */
+
+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 {
+ 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 {
+ 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(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(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(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
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
index c61c4c5ad..b223e28c8 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
@@ -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()
diff --git a/app/src/main/res/drawable/ic_import_24.xml b/app/src/main/res/drawable/ic_import_24.xml
new file mode 100644
index 000000000..f63386171
--- /dev/null
+++ b/app/src/main/res/drawable/ic_import_24.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index f1b5c8c80..d24c1a2d3 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -8,6 +8,7 @@
android:background="?attr/colorSurface"
android:transitionGroup="true">
+
@@ -147,18 +148,43 @@
+ android:layout_gravity="bottom|end"
+ android:layout_height="match_parent">
-
+
+
+ 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" />
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/new_playlist_actions.xml b/app/src/main/res/menu/new_playlist_actions.xml
new file mode 100644
index 000000000..7b9916426
--- /dev/null
+++ b/app/src/main/res/menu/new_playlist_actions.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c7866d184..f00389de8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -88,6 +88,8 @@
Playlist
Playlists
New playlist
+ Empty playlist
+ Imported playlist
Rename
Rename playlist
Delete