Add looping
Add the ability to loop songs either once or infinitely.
This commit is contained in:
parent
ce96dd6e94
commit
535fc95f71
10 changed files with 135 additions and 4 deletions
|
@ -15,6 +15,7 @@ import androidx.navigation.fragment.findNavController
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentPlaybackBinding
|
||||
import org.oxycblt.auxio.playback.queue.QueueFragment
|
||||
import org.oxycblt.auxio.playback.state.LoopMode
|
||||
import org.oxycblt.auxio.theme.accent
|
||||
import org.oxycblt.auxio.theme.disable
|
||||
import org.oxycblt.auxio.theme.enable
|
||||
|
@ -123,6 +124,23 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
|||
}
|
||||
}
|
||||
|
||||
playbackModel.loopMode.observe(viewLifecycleOwner) {
|
||||
when (it) {
|
||||
LoopMode.NONE -> {
|
||||
binding.playbackLoop.imageTintList = controlColor
|
||||
binding.playbackLoop.setImageResource(R.drawable.ic_loop)
|
||||
}
|
||||
LoopMode.ONCE -> {
|
||||
binding.playbackLoop.imageTintList = accentColor
|
||||
binding.playbackLoop.setImageResource(R.drawable.ic_loop_one)
|
||||
}
|
||||
LoopMode.INFINITE -> {
|
||||
binding.playbackLoop.imageTintList = accentColor
|
||||
binding.playbackLoop.setImageResource(R.drawable.ic_loop)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
playbackModel.isSeeking.observe(viewLifecycleOwner) {
|
||||
// Highlight the current duration if the user is seeking, and revert it if not.
|
||||
if (it) {
|
||||
|
|
|
@ -34,6 +34,7 @@ import kotlinx.coroutines.launch
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.toURI
|
||||
import org.oxycblt.auxio.playback.state.LoopMode
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateCallback
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
|
||||
|
@ -131,6 +132,15 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
// If the song loops while in the LOOP_ONCE mode, then stop looping after that.
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT &&
|
||||
playbackManager.loopMode == LoopMode.ONCE
|
||||
) {
|
||||
playbackManager.setLoopMode(LoopMode.NONE)
|
||||
}
|
||||
}
|
||||
|
||||
// --- PLAYBACK STATE CALLBACK OVERRIDES ---
|
||||
|
||||
override fun onSongUpdate(song: Song?) {
|
||||
|
@ -160,6 +170,17 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onLoopUpdate(mode: LoopMode) {
|
||||
when (mode) {
|
||||
LoopMode.NONE -> {
|
||||
player.repeatMode = Player.REPEAT_MODE_OFF
|
||||
}
|
||||
else -> {
|
||||
player.repeatMode = Player.REPEAT_MODE_ONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSeekConfirm(position: Long) {
|
||||
player.seekTo(position * 1000)
|
||||
}
|
||||
|
@ -253,8 +274,8 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
|||
return notif
|
||||
}
|
||||
|
||||
// Broadcast Receiver for receiving system events [E.G Headphones connecte/disconnected
|
||||
inner class SystemEventReceiver : BroadcastReceiver() {
|
||||
// BroadcastReceiver for receiving system events [E.G Headphones connected/disconnected]
|
||||
private inner class SystemEventReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.oxycblt.auxio.music.Artist
|
|||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.toDuration
|
||||
import org.oxycblt.auxio.playback.state.LoopMode
|
||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateCallback
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
|
@ -40,6 +41,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateCallback {
|
|||
private val mIsShuffling = MutableLiveData(false)
|
||||
val isShuffling: LiveData<Boolean> get() = mIsShuffling
|
||||
|
||||
private val mLoopMode = MutableLiveData(LoopMode.NONE)
|
||||
val loopMode: LiveData<LoopMode> get() = mLoopMode
|
||||
|
||||
// Other
|
||||
private val mIsSeeking = MutableLiveData(false)
|
||||
val isSeeking: LiveData<Boolean> get() = mIsSeeking
|
||||
|
@ -176,6 +180,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateCallback {
|
|||
playbackManager.setShuffleStatus(!playbackManager.isShuffling)
|
||||
}
|
||||
|
||||
fun incrementLoopStatus() {
|
||||
playbackManager.setLoopMode(playbackManager.loopMode.increment())
|
||||
}
|
||||
|
||||
// --- OTHER FUNCTIONS ---
|
||||
|
||||
fun setSeekingStatus(value: Boolean) {
|
||||
|
@ -218,6 +226,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateCallback {
|
|||
mIsShuffling.value = isShuffling
|
||||
}
|
||||
|
||||
override fun onLoopUpdate(mode: LoopMode) {
|
||||
mLoopMode.value = mode
|
||||
}
|
||||
|
||||
private fun restorePlaybackState() {
|
||||
Log.d(this::class.simpleName, "Attempting to restore playback state.")
|
||||
|
||||
|
@ -227,5 +239,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateCallback {
|
|||
mIndex.value = playbackManager.index
|
||||
mIsPlaying.value = playbackManager.isPlaying
|
||||
mIsShuffling.value = playbackManager.isShuffling
|
||||
mLoopMode.value = playbackManager.loopMode
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package org.oxycblt.auxio.playback.state
|
||||
|
||||
enum class LoopMode {
|
||||
NONE, ONCE, INFINITE;
|
||||
|
||||
fun increment(): LoopMode {
|
||||
return when (this) {
|
||||
NONE -> ONCE
|
||||
ONCE -> INFINITE
|
||||
INFINITE -> NONE
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,9 +6,10 @@ interface PlaybackStateCallback {
|
|||
fun onSongUpdate(song: Song?) {}
|
||||
fun onPositionUpdate(position: Long) {}
|
||||
fun onQueueUpdate(queue: MutableList<Song>) {}
|
||||
fun onIndexUpdate(index: Int) {}
|
||||
fun onPlayingUpdate(isPlaying: Boolean) {}
|
||||
fun onShuffleUpdate(isShuffling: Boolean) {}
|
||||
fun onIndexUpdate(index: Int) {}
|
||||
fun onLoopUpdate(mode: LoopMode) {}
|
||||
|
||||
// Service callbacks
|
||||
fun onSeekConfirm(position: Long) {}
|
||||
|
|
|
@ -53,6 +53,11 @@ class PlaybackStateManager {
|
|||
callbacks.forEach { it.onShuffleUpdate(value) }
|
||||
}
|
||||
private var mShuffleSeed = -1L
|
||||
private var mLoopMode = LoopMode.NONE
|
||||
set(value) {
|
||||
field = value
|
||||
callbacks.forEach { it.onLoopUpdate(value) }
|
||||
}
|
||||
|
||||
val song: Song? get() = mSong
|
||||
val position: Long get() = mPosition
|
||||
|
@ -60,6 +65,7 @@ class PlaybackStateManager {
|
|||
val index: Int get() = mIndex
|
||||
val isPlaying: Boolean get() = mIsPlaying
|
||||
val isShuffling: Boolean get() = mIsShuffling
|
||||
val loopMode: LoopMode get() = mLoopMode
|
||||
|
||||
// --- CALLBACKS ---
|
||||
|
||||
|
@ -90,6 +96,7 @@ class PlaybackStateManager {
|
|||
|
||||
mMode = mode
|
||||
|
||||
resetLoopMode()
|
||||
updatePlayback(song)
|
||||
|
||||
mQueue = when (mode) {
|
||||
|
@ -144,6 +151,8 @@ class PlaybackStateManager {
|
|||
else -> error("what")
|
||||
}
|
||||
|
||||
resetLoopMode()
|
||||
|
||||
updatePlayback(mQueue[0])
|
||||
|
||||
mIndex = 0
|
||||
|
@ -185,10 +194,12 @@ class PlaybackStateManager {
|
|||
// --- QUEUE FUNCTIONS ---
|
||||
|
||||
fun next() {
|
||||
resetLoopMode()
|
||||
|
||||
if (mIndex < mQueue.lastIndex) {
|
||||
mIndex = mIndex.inc()
|
||||
} else {
|
||||
// TODO: Implement option so that the playlist loops instead of stops
|
||||
// TODO: Implement option to make the playlist loop instead of stop
|
||||
mQueue = mutableListOf()
|
||||
mSong = null
|
||||
|
||||
|
@ -205,6 +216,8 @@ class PlaybackStateManager {
|
|||
mIndex = mIndex.dec()
|
||||
}
|
||||
|
||||
resetLoopMode()
|
||||
|
||||
updatePlayback(mQueue[mIndex])
|
||||
|
||||
forceQueueUpdate()
|
||||
|
@ -318,6 +331,17 @@ class PlaybackStateManager {
|
|||
}
|
||||
}
|
||||
|
||||
fun setLoopMode(mode: LoopMode) {
|
||||
mLoopMode = mode
|
||||
}
|
||||
|
||||
private fun resetLoopMode() {
|
||||
// Reset the loop mode froM ONCE if needed.
|
||||
if (mLoopMode == LoopMode.ONCE) {
|
||||
mLoopMode = LoopMode.NONE
|
||||
}
|
||||
}
|
||||
|
||||
// --- ORDERING FUNCTIONS ---
|
||||
|
||||
private fun orderSongsInAlbum(album: Album): MutableList<Song> {
|
||||
|
@ -365,5 +389,9 @@ class PlaybackStateManager {
|
|||
return newInstance
|
||||
}
|
||||
}
|
||||
|
||||
const val LOOP_NONE = 0
|
||||
const val LOOP_ONCE = 1
|
||||
const val LOOP_ENDLESS = 2
|
||||
}
|
||||
}
|
||||
|
|
11
app/src/main/res/drawable/ic_loop.xml
Normal file
11
app/src/main/res/drawable/ic_loop.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="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?android:attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/>
|
||||
</vector>
|
11
app/src/main/res/drawable/ic_loop_one.xml
Normal file
11
app/src/main/res/drawable/ic_loop_one.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="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?android:attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4zM13,15L13,9h-1l-2,1v1h1.5v4L13,15z"/>
|
||||
</vector>
|
|
@ -201,5 +201,19 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/playback_play_pause" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/playback_loop"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="@dimen/size_play_pause_compact"
|
||||
android:layout_height="@dimen/size_play_pause_compact"
|
||||
android:background="@drawable/ui_unbounded_ripple"
|
||||
android:src="@drawable/ic_loop"
|
||||
android:layout_marginStart="@dimen/margin_mid_large"
|
||||
android:onClick="@{() -> playbackModel.incrementLoopStatus()}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/playback_play_pause"
|
||||
android:contentDescription="@string/description_loop"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
|
@ -50,6 +50,7 @@
|
|||
<string name="description_skip_prev">Skip to last song</string>
|
||||
<string name="description_shuffle_on">Turn shuffle on</string>
|
||||
<string name="description_shuffle_off">Turn shuffle off</string>
|
||||
<string name="description_loop">Change Loop Mode</string>
|
||||
|
||||
<!-- Placeholder Namespace | Placeholder values -->
|
||||
<string name="placeholder_genre">Unknown Genre</string>
|
||||
|
|
Loading…
Reference in a new issue