playback: add shuffle shortcut
Add a shortcut to shuffle all songs. This is likely the only static shortcut Auxio will have. Top tracks and recently added are completely useless for me, so I will never add them. I may add more dynamic shortcuts for recently played items, however. Note that we use a basic black shuffle icon here. I will not add icon customization to these shortcuts.
This commit is contained in:
parent
20e7b25808
commit
29fe849565
11 changed files with 73 additions and 22 deletions
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
## dev
|
## dev
|
||||||
|
|
||||||
|
#### What's New
|
||||||
|
- Added a shuffle shortcut
|
||||||
|
|
||||||
#### What's Fixed
|
#### What's Fixed
|
||||||
- Fixed broken tablet layouts
|
- Fixed broken tablet layouts
|
||||||
- Fixed seam that would appear on some album covers
|
- Fixed seam that would appear on some album covers
|
||||||
|
|
|
@ -14,8 +14,6 @@
|
||||||
|
|
||||||
<queries />
|
<queries />
|
||||||
|
|
||||||
<!-- TODO: A "shuffle" shortcut -->
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Note: We have to simultaneously define the fullBackupContent and dataExtractionRules
|
Note: We have to simultaneously define the fullBackupContent and dataExtractionRules
|
||||||
fields, as there is no way to make version-specific manifests. This should be okay,
|
fields, as there is no way to make version-specific manifests. This should be okay,
|
||||||
|
|
|
@ -18,6 +18,10 @@
|
||||||
package org.oxycblt.auxio
|
package org.oxycblt.auxio
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.content.pm.ShortcutInfoCompat
|
||||||
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
import coil.request.CachePolicy
|
import coil.request.CachePolicy
|
||||||
|
@ -27,12 +31,30 @@ import org.oxycblt.auxio.image.CrossfadeTransitionFactory
|
||||||
import org.oxycblt.auxio.image.GenreImageFetcher
|
import org.oxycblt.auxio.image.GenreImageFetcher
|
||||||
import org.oxycblt.auxio.image.MusicKeyer
|
import org.oxycblt.auxio.image.MusicKeyer
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
@Suppress("UNUSED")
|
|
||||||
class AuxioApp : Application(), ImageLoaderFactory {
|
class AuxioApp : Application(), ImageLoaderFactory {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
logD(BuildConfig.APPLICATION_ID + ".MainActivity")
|
||||||
|
|
||||||
|
// Adding static shortcuts in a dynamic manner is better than declaring them
|
||||||
|
// manually, as it will properly handle the difference between debug and release
|
||||||
|
// Auxio instances.
|
||||||
|
ShortcutManagerCompat.addDynamicShortcuts(
|
||||||
|
this,
|
||||||
|
listOf(
|
||||||
|
ShortcutInfoCompat.Builder(this, SHORTCUT_SHUFFLE_ID)
|
||||||
|
.setShortLabel(getString(R.string.lbl_shuffle_shortcut_short))
|
||||||
|
.setLongLabel(getString(R.string.lbl_shuffle_shortcut_long))
|
||||||
|
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_shuffle_shortcut))
|
||||||
|
.setIntent(
|
||||||
|
Intent(this, MainActivity::class.java).apply {
|
||||||
|
action = INTENT_KEY_SHORTCUT_SHUFFLE
|
||||||
|
})
|
||||||
|
.build()))
|
||||||
|
|
||||||
// Init SettingsManager here so that there aren't any race conditions
|
// Init SettingsManager here so that there aren't any race conditions
|
||||||
// [e.g PlaybackService gets SettingsManager before activity can init SettingsManager]
|
// [e.g PlaybackService gets SettingsManager before activity can init SettingsManager]
|
||||||
SettingsManager.init(applicationContext)
|
SettingsManager.init(applicationContext)
|
||||||
|
@ -51,4 +73,9 @@ class AuxioApp : Application(), ImageLoaderFactory {
|
||||||
.diskCachePolicy(CachePolicy.DISABLED) // Not downloading anything, so no disk-caching
|
.diskCachePolicy(CachePolicy.DISABLED) // Not downloading anything, so no disk-caching
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SHORTCUT_SHUFFLE_ID = "shortcut_shuffle"
|
||||||
|
const val INTENT_KEY_SHORTCUT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE_ALL"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,21 +71,31 @@ class MainActivity : AppCompatActivity() {
|
||||||
startService(Intent(this, IndexerService::class.java))
|
startService(Intent(this, IndexerService::class.java))
|
||||||
startService(Intent(this, PlaybackService::class.java))
|
startService(Intent(this, PlaybackService::class.java))
|
||||||
|
|
||||||
// If we have a file URI already, open it. Otherwise, restore the playback state.
|
// If we have a valid intent, use that. Otherwise, restore the playback state.
|
||||||
val action =
|
val action = intentToDelayedAction(intent) ?: PlaybackViewModel.DelayedAction.RestoreState
|
||||||
retrieveViewUri(intent)?.let { PlaybackViewModel.DelayedAction.Open(it) }
|
|
||||||
?: PlaybackViewModel.DelayedAction.RestoreState
|
|
||||||
|
|
||||||
playbackModel.performAction(this, action)
|
playbackModel.startDelayedAction(this, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
// See if the new intent is a file intent. If so, open it.
|
intentToDelayedAction(intent)?.let { action ->
|
||||||
val uri = retrieveViewUri(intent)
|
playbackModel.startDelayedAction(this, action)
|
||||||
if (uri != null) {
|
}
|
||||||
playbackModel.performAction(this, PlaybackViewModel.DelayedAction.Open(uri))
|
}
|
||||||
|
|
||||||
|
private fun intentToDelayedAction(intent: Intent?): PlaybackViewModel.DelayedAction? {
|
||||||
|
if (intent == null || intent.getBooleanExtra(KEY_INTENT_USED, false)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
intent.putExtra(KEY_INTENT_USED, true)
|
||||||
|
|
||||||
|
return when (intent.action) {
|
||||||
|
Intent.ACTION_VIEW -> intent.data?.let { PlaybackViewModel.DelayedAction.Open(it) }
|
||||||
|
AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> PlaybackViewModel.DelayedAction.ShuffleAll
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ class IndexerService : Service(), Indexer.Callback {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
private val indexScope = CoroutineScope(serviceJob + Dispatchers.Default)
|
private val indexScope = CoroutineScope(serviceJob + Dispatchers.Main)
|
||||||
|
|
||||||
private var isForeground = false
|
private var isForeground = false
|
||||||
private lateinit var notification: IndexerNotification
|
private lateinit var notification: IndexerNotification
|
||||||
|
|
|
@ -189,7 +189,7 @@ class PlaybackPanelFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlaying(isPlaying: Boolean) {
|
private fun updatePlaying(isPlaying: Boolean) {
|
||||||
requireBinding().playbackPlayPause.isActivated = isPlaying
|
requireBinding().playbackPlayPause.apply { isActivated = isPlaying }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateShuffled(isShuffled: Boolean) {
|
private fun updateShuffled(isShuffled: Boolean) {
|
||||||
|
|
|
@ -144,12 +144,12 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore
|
||||||
* usually alongside a context too. Examples include:
|
* usually alongside a context too. Examples include:
|
||||||
* - Opening files
|
* - Opening files
|
||||||
* - Restoring the playback state
|
* - Restoring the playback state
|
||||||
* - (Future) app shortcuts
|
* - App shortcuts
|
||||||
*
|
*
|
||||||
* We would normally want to put this kind of functionality into PlaybackService, but it's
|
* We would normally want to put this kind of functionality into PlaybackService, but it's
|
||||||
* lifecycle makes that more or less impossible.
|
* lifecycle makes that more or less impossible.
|
||||||
*/
|
*/
|
||||||
fun performAction(context: Context, action: DelayedAction) {
|
fun startDelayedAction(context: Context, action: DelayedAction) {
|
||||||
val library = musicStore.library
|
val library = musicStore.library
|
||||||
val actionImpl = DelayedActionImpl(context.applicationContext, action)
|
val actionImpl = DelayedActionImpl(context.applicationContext, action)
|
||||||
if (library != null) {
|
if (library != null) {
|
||||||
|
@ -166,6 +166,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore
|
||||||
viewModelScope.launch { playbackManager.restoreState(action.context) }
|
viewModelScope.launch { playbackManager.restoreState(action.context) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is DelayedAction.ShuffleAll -> shuffleAll()
|
||||||
is DelayedAction.Open -> {
|
is DelayedAction.Open -> {
|
||||||
library.findSongForUri(action.context, action.inner.uri)?.let(::play)
|
library.findSongForUri(action.context, action.inner.uri)?.let(::play)
|
||||||
}
|
}
|
||||||
|
@ -284,6 +285,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore
|
||||||
/** An action delayed until the complete load of the music library. */
|
/** An action delayed until the complete load of the music library. */
|
||||||
sealed class DelayedAction {
|
sealed class DelayedAction {
|
||||||
object RestoreState : DelayedAction()
|
object RestoreState : DelayedAction()
|
||||||
|
object ShuffleAll : DelayedAction()
|
||||||
data class Open(val uri: Uri) : DelayedAction()
|
data class Open(val uri: Uri) : DelayedAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
app/src/main/res/drawable/ic_shuffle_shortcut.xml
Normal file
10
app/src/main/res/drawable/ic_shuffle_shortcut.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?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="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/black"
|
||||||
|
android:pathData="M10.59,9.17L5.41,4 4,5.41l5.17,5.17 1.42,-1.41zM14.5,4l2.04,2.04L4,18.59 5.41,20 17.96,7.46 20,9.5L20,4h-5.5zM14.83,13.41l-1.41,1.41 3.13,3.13L14.5,20L20,20v-5.5l-2.04,2.04 -3.13,-3.13z" />
|
||||||
|
</vector>
|
|
@ -8,4 +8,4 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||||
tools:listitem="@layout/item_artist" />
|
tools:listitem="@layout/item_parent" />
|
||||||
|
|
|
@ -17,11 +17,7 @@
|
||||||
app:popExitAnim="@anim/nav_slide_pop_exit_anim" />
|
app:popExitAnim="@anim/nav_slide_pop_exit_anim" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_show_settings"
|
android:id="@+id/action_show_settings"
|
||||||
app:destination="@id/settings_fragment"
|
app:destination="@id/settings_fragment" />
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_show_about"
|
android:id="@+id/action_show_about"
|
||||||
app:destination="@id/about_fragment"
|
app:destination="@id/about_fragment"
|
||||||
|
|
|
@ -59,6 +59,11 @@
|
||||||
|
|
||||||
<string name="lbl_state_saved">State saved</string>
|
<string name="lbl_state_saved">State saved</string>
|
||||||
|
|
||||||
|
<!-- Limit to 10 characters -->
|
||||||
|
<string name="lbl_shuffle_shortcut_short">Shuffle</string>
|
||||||
|
<!-- Limit to 25 characters -->
|
||||||
|
<string name="lbl_shuffle_shortcut_long">Shuffle All</string>
|
||||||
|
|
||||||
<!-- Actual string: Ok -->
|
<!-- Actual string: Ok -->
|
||||||
<string name="lbl_ok">@android:string/ok</string>
|
<string name="lbl_ok">@android:string/ok</string>
|
||||||
<!-- Actual string: Cancel -->
|
<!-- Actual string: Cancel -->
|
||||||
|
|
Loading…
Reference in a new issue