Better separation between FOSS and full version. Chromecast playback, basic queue management and playback controls are functional.
This commit is contained in:
parent
3654e28c0c
commit
9ed7eab761
|
@ -2,10 +2,18 @@ package com.github.apognu.otter
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import com.github.apognu.otter.utils.log
|
import com.github.apognu.otter.playback.PlayerService
|
||||||
|
import com.github.apognu.otter.utils.CastInterface
|
||||||
|
|
||||||
object Cast {
|
class Cast(val context: Context, val switchListener: PlayerService.OnPlayerSwitchListener, playerEventListener: PlayerService.PlayerEventListener) : CastInterface {
|
||||||
|
companion object {
|
||||||
fun init(context: Context) {}
|
fun init(context: Context) {}
|
||||||
fun setupButton(context: Context, menu: Menu?) {}
|
fun setupButton(context: Context, menu: Menu?) {}
|
||||||
}
|
|
||||||
|
|
||||||
|
fun get(
|
||||||
|
context: Context,
|
||||||
|
playerSwitchListener: PlayerService.OnPlayerSwitchListener,
|
||||||
|
playerEventListener: PlayerService.PlayerEventListener
|
||||||
|
): Cast? = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_search"
|
||||||
|
android:icon="@drawable/search"
|
||||||
|
android:title="@string/toolbar_search"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_only_my_music"
|
||||||
|
android:checkable="true"
|
||||||
|
android:title="@string/only_my_music"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/settings"
|
||||||
|
android:icon="@drawable/settings"
|
||||||
|
android:iconTint="@android:color/white"
|
||||||
|
android:title="@string/title_settings"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
</menu>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_queue"
|
||||||
|
android:icon="@drawable/queue"
|
||||||
|
android:title="@string/playback_queue"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_search"
|
||||||
|
android:icon="@drawable/search"
|
||||||
|
android:title="@string/toolbar_search"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_only_my_music"
|
||||||
|
android:checkable="true"
|
||||||
|
android:title="@string/only_my_music"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_downloads"
|
||||||
|
android:icon="@drawable/downloads"
|
||||||
|
android:title="@string/title_downloads"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/settings"
|
||||||
|
android:icon="@drawable/settings"
|
||||||
|
android:title="@string/title_settings"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
</menu>
|
|
@ -1,11 +1,32 @@
|
||||||
package com.github.apognu.otter
|
package com.github.apognu.otter
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
|
import com.github.apognu.otter.playback.PlayerService
|
||||||
|
import com.github.apognu.otter.utils.AppContext
|
||||||
|
import com.github.apognu.otter.utils.CastInterface
|
||||||
|
import com.github.apognu.otter.utils.Track
|
||||||
|
import com.github.apognu.otter.utils.mustNormalizeUrl
|
||||||
|
import com.google.android.exoplayer2.Player
|
||||||
|
import com.google.android.exoplayer2.Timeline
|
||||||
|
import com.google.android.exoplayer2.ext.cast.CastPlayer
|
||||||
|
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener
|
||||||
|
import com.google.android.gms.cast.MediaInfo
|
||||||
|
import com.google.android.gms.cast.MediaMetadata
|
||||||
|
import com.google.android.gms.cast.MediaQueueItem
|
||||||
import com.google.android.gms.cast.framework.CastButtonFactory
|
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||||
import com.google.android.gms.cast.framework.CastContext
|
import com.google.android.gms.cast.framework.CastContext
|
||||||
|
import com.google.android.gms.common.images.WebImage
|
||||||
|
import com.preference.PowerPreference
|
||||||
|
|
||||||
object Cast {
|
fun Player.onCast(): CastPlayer? {
|
||||||
|
return if (this is CastPlayer) this
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
class Cast(val context: Context, val switchListener: PlayerService.OnPlayerSwitchListener, playerEventListener: PlayerService.PlayerEventListener) : CastInterface {
|
||||||
|
companion object {
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
CastContext.getSharedInstance(context)
|
CastContext.getSharedInstance(context)
|
||||||
}
|
}
|
||||||
|
@ -13,5 +34,109 @@ object Cast {
|
||||||
fun setupButton(context: Context, menu: Menu?) {
|
fun setupButton(context: Context, menu: Menu?) {
|
||||||
CastButtonFactory.setUpMediaRouteButton(context, menu, R.id.cast)
|
CastButtonFactory.setUpMediaRouteButton(context, menu, R.id.cast)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun get(
|
||||||
|
context: Context,
|
||||||
|
playerSwitchListener: PlayerService.OnPlayerSwitchListener,
|
||||||
|
playerEventListener: PlayerService.PlayerEventListener
|
||||||
|
): Cast = Cast(context, playerSwitchListener, playerEventListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val player: Player
|
||||||
|
|
||||||
|
init {
|
||||||
|
player = CastPlayer(CastContext.getSharedInstance(context)).apply {
|
||||||
|
addListener(playerEventListener)
|
||||||
|
setSessionAvailabilityListener(CastSessionListener())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPlayer(context: Context): Player = player
|
||||||
|
|
||||||
|
override fun replaceQueue(tracks: List<Track>) {
|
||||||
|
player.onCast()?.let { castPlayer ->
|
||||||
|
tracks
|
||||||
|
.map { track -> buildMediaQueueItem(track) }
|
||||||
|
.apply {
|
||||||
|
castPlayer.loadItems(this.toTypedArray(), 0, 0, Player.REPEAT_MODE_OFF)
|
||||||
|
castPlayer.playWhenReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addToQueue(tracks: List<Track>) {
|
||||||
|
player.onCast()?.let { castPlayer ->
|
||||||
|
tracks
|
||||||
|
.map { track -> buildMediaQueueItem(track) }
|
||||||
|
.forEach {
|
||||||
|
castPlayer.addItems(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun insertNext(track: Track, current: Int) {
|
||||||
|
player.onCast()?.let { castPlayer ->
|
||||||
|
val period = Timeline.Period().run {
|
||||||
|
player.currentTimeline.getPeriod(current + 1, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
castPlayer.addItems(period.id.toString().toInt(), buildMediaQueueItem(track))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(index: Int) {
|
||||||
|
player.onCast()?.let { castPlayer ->
|
||||||
|
val period = Timeline.Period().run {
|
||||||
|
player.currentTimeline.getPeriod(index, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
castPlayer.removeItem(period.id.toString().toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun move(oldPosition: Int, newPosition: Int) {
|
||||||
|
player.onCast()?.let { castPlayer ->
|
||||||
|
val period = Timeline.Period().run {
|
||||||
|
player.currentTimeline.getPeriod(oldPosition, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
castPlayer.moveItem(period.id.toString().toInt(), newPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildMediaQueueItem(track: Track): MediaQueueItem {
|
||||||
|
val listenUrl = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||||
|
val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("listen_token", "")
|
||||||
|
val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK).apply {
|
||||||
|
putString(MediaMetadata.KEY_ARTIST, track.artist.name)
|
||||||
|
putString(MediaMetadata.KEY_ALBUM_TITLE, track.album.title)
|
||||||
|
putString(MediaMetadata.KEY_TITLE, track.title)
|
||||||
|
|
||||||
|
addImage(WebImage(Uri.parse(mustNormalizeUrl(track.album.cover()))))
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = Uri.parse(listenUrl)
|
||||||
|
.buildUpon()
|
||||||
|
.appendQueryParameter("token", token)
|
||||||
|
.build()
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
val mediaInfo = MediaInfo.Builder(url)
|
||||||
|
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||||
|
.setMetadata(metadata)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return MediaQueueItem.Builder(mediaInfo).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class CastSessionListener : SessionAvailabilityListener {
|
||||||
|
override fun onCastSessionAvailable() {
|
||||||
|
switchListener.switchToRemote()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCastSessionUnavailable() {
|
||||||
|
switchListener.switchToLocal()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,18 +2,17 @@
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<!-- <item
|
<item
|
||||||
android:id="@+id/cast"
|
android:id="@+id/cast"
|
||||||
android:iconTint="@android:color/white"
|
android:title="Cast!"
|
||||||
android:title="@string/toolbar_cast"
|
|
||||||
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
|
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
|
||||||
app:showAsAction="ifRoom" /> -->
|
app:showAsAction="always" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/nav_search"
|
android:id="@+id/nav_search"
|
||||||
android:icon="@drawable/search"
|
android:icon="@drawable/search"
|
||||||
android:title="@string/toolbar_search"
|
android:title="@string/toolbar_search"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="always" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/nav_only_my_music"
|
android:id="@+id/nav_only_my_music"
|
||||||
|
@ -24,7 +23,6 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/settings"
|
android:id="@+id/settings"
|
||||||
android:icon="@drawable/settings"
|
android:icon="@drawable/settings"
|
||||||
android:iconTint="@android:color/white"
|
|
||||||
android:title="@string/title_settings"
|
android:title="@string/title_settings"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<style name="AppTheme" parent="AppTheme.Base">
|
||||||
|
<item name="mediaRouteTheme">@style/AppTheme.MediaRouteTheme</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.MediaRouteTheme" parent="Theme.MediaRouter">
|
||||||
|
<item name="mediaRouteButtonStyle">@style/AppTheme.MediaRouteTheme.ButtonStyle</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.MediaRouteTheme.ButtonStyle" parent="Widget.MediaRouter.Light.MediaRouteButton">
|
||||||
|
<item name="mediaRouteButtonTint">@android:color/white</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
|
@ -12,6 +12,7 @@ import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
import com.github.apognu.otter.Cast
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
import com.google.android.exoplayer2.C
|
import com.google.android.exoplayer2.C
|
||||||
|
@ -30,6 +31,11 @@ import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class PlayerService : Service() {
|
class PlayerService : Service() {
|
||||||
|
interface OnPlayerSwitchListener {
|
||||||
|
fun switchToLocal()
|
||||||
|
fun switchToRemote()
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var queue: QueueManager
|
private lateinit var queue: QueueManager
|
||||||
private val jobs = mutableListOf<Job>()
|
private val jobs = mutableListOf<Job>()
|
||||||
|
|
||||||
|
@ -40,7 +46,10 @@ class PlayerService : Service() {
|
||||||
|
|
||||||
private lateinit var mediaControlsManager: MediaControlsManager
|
private lateinit var mediaControlsManager: MediaControlsManager
|
||||||
private lateinit var mediaSession: MediaSessionCompat
|
private lateinit var mediaSession: MediaSessionCompat
|
||||||
private lateinit var player: SimpleExoPlayer
|
|
||||||
|
private lateinit var player: Player
|
||||||
|
private lateinit var localPlayer: SimpleExoPlayer
|
||||||
|
private var cast: Cast? = null
|
||||||
|
|
||||||
private lateinit var playerEventListener: PlayerEventListener
|
private lateinit var playerEventListener: PlayerEventListener
|
||||||
private val headphonesUnpluggedReceiver = HeadphonesUnpluggedReceiver()
|
private val headphonesUnpluggedReceiver = HeadphonesUnpluggedReceiver()
|
||||||
|
@ -58,7 +67,6 @@ class PlayerService : Service() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
queue = QueueManager(this)
|
|
||||||
radioPlayer = RadioPlayer(this)
|
radioPlayer = RadioPlayer(this)
|
||||||
|
|
||||||
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
@ -85,13 +93,15 @@ class PlayerService : Service() {
|
||||||
|
|
||||||
mediaControlsManager = MediaControlsManager(this, mediaSession)
|
mediaControlsManager = MediaControlsManager(this, mediaSession)
|
||||||
|
|
||||||
player = SimpleExoPlayer.Builder(this).build().apply {
|
localPlayer = SimpleExoPlayer.Builder(this).build().apply {
|
||||||
playWhenReady = false
|
playWhenReady = false
|
||||||
|
|
||||||
playerEventListener = PlayerEventListener().also {
|
playerEventListener = PlayerEventListener().also {
|
||||||
addListener(it)
|
addListener(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cast = Cast.get(this@PlayerService, PlayerSwitchListener(), playerEventListener)
|
||||||
|
|
||||||
MediaSessionConnector(mediaSession).also {
|
MediaSessionConnector(mediaSession).also {
|
||||||
it.setPlayer(this)
|
it.setPlayer(this)
|
||||||
it.setMediaButtonEventHandler { player, _, mediaButtonEvent ->
|
it.setMediaButtonEventHandler { player, _, mediaButtonEvent ->
|
||||||
|
@ -111,8 +121,11 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
player = if (cast?.isCastSessionAvailable() == true) cast!!.getPlayer(this) else localPlayer
|
||||||
|
queue = QueueManager(this, cast)
|
||||||
|
|
||||||
if (queue.current > -1) {
|
if (queue.current > -1) {
|
||||||
player.prepare(queue.datasources, true, true)
|
player.onLocal()?.prepare(queue.datasources, true, true)
|
||||||
|
|
||||||
Cache.get(this, "progress")?.let { progress ->
|
Cache.get(this, "progress")?.let { progress ->
|
||||||
player.seekTo(queue.current, progress.readLine().toLong())
|
player.seekTo(queue.current, progress.readLine().toLong())
|
||||||
|
@ -143,7 +156,7 @@ class PlayerService : Service() {
|
||||||
if (!command.fromRadio) radioPlayer.stop()
|
if (!command.fromRadio) radioPlayer.stop()
|
||||||
|
|
||||||
queue.replace(command.queue)
|
queue.replace(command.queue)
|
||||||
player.prepare(queue.datasources, true, true)
|
player.onLocal()?.prepare(queue.datasources, true, true)
|
||||||
|
|
||||||
state(true)
|
state(true)
|
||||||
|
|
||||||
|
@ -262,7 +275,7 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state && player.playbackState == Player.STATE_IDLE) {
|
if (state && player.playbackState == Player.STATE_IDLE) {
|
||||||
player.prepare(queue.datasources)
|
player.onLocal()?.prepare(queue.datasources)
|
||||||
}
|
}
|
||||||
|
|
||||||
var allowed = !state
|
var allowed = !state
|
||||||
|
@ -394,7 +407,7 @@ class PlayerService : Service() {
|
||||||
EventBus.send(Event.PlaybackError(getString(R.string.error_playback)))
|
EventBus.send(Event.PlaybackError(getString(R.string.error_playback)))
|
||||||
|
|
||||||
queue.current++
|
queue.current++
|
||||||
player.prepare(queue.datasources, true, true)
|
player.onLocal()?.prepare(queue.datasources, true, true)
|
||||||
player.seekTo(queue.current, 0)
|
player.seekTo(queue.current, 0)
|
||||||
player.playWhenReady = true
|
player.playWhenReady = true
|
||||||
|
|
||||||
|
@ -406,7 +419,7 @@ class PlayerService : Service() {
|
||||||
override fun onAudioFocusChange(focus: Int) {
|
override fun onAudioFocusChange(focus: Int) {
|
||||||
when (focus) {
|
when (focus) {
|
||||||
AudioManager.AUDIOFOCUS_GAIN -> {
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||||
player.volume = 1f
|
player.onLocal()?.volume = 1f
|
||||||
|
|
||||||
state(stateWhenLostFocus)
|
state(stateWhenLostFocus)
|
||||||
stateWhenLostFocus = false
|
stateWhenLostFocus = false
|
||||||
|
@ -424,9 +437,21 @@ class PlayerService : Service() {
|
||||||
|
|
||||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||||
stateWhenLostFocus = player.playWhenReady
|
stateWhenLostFocus = player.playWhenReady
|
||||||
player.volume = 0.3f
|
player.onLocal()?.volume = 0.3f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class PlayerSwitchListener : OnPlayerSwitchListener {
|
||||||
|
override fun switchToLocal() {
|
||||||
|
player = localPlayer
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun switchToRemote() {
|
||||||
|
cast?.let { cast ->
|
||||||
|
player = cast.getPlayer(this@PlayerService)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory
|
||||||
import com.google.android.exoplayer2.util.Util
|
import com.google.android.exoplayer2.util.Util
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
|
||||||
class QueueManager(val context: Context) {
|
class QueueManager(val context: Context, val cast: CastInterface?) {
|
||||||
var metadata: MutableList<Track> = mutableListOf()
|
var metadata: MutableList<Track> = mutableListOf()
|
||||||
val datasources = ConcatenatingMediaSource()
|
val datasources = ConcatenatingMediaSource()
|
||||||
var current = -1
|
var current = -1
|
||||||
|
@ -84,6 +84,8 @@ class QueueManager(val context: Context) {
|
||||||
datasources.clear()
|
datasources.clear()
|
||||||
datasources.addMediaSources(sources)
|
datasources.addMediaSources(sources)
|
||||||
|
|
||||||
|
cast?.replaceQueue(tracks)
|
||||||
|
|
||||||
persist()
|
persist()
|
||||||
|
|
||||||
EventBus.send(Event.QueueChanged)
|
EventBus.send(Event.QueueChanged)
|
||||||
|
@ -102,6 +104,8 @@ class QueueManager(val context: Context) {
|
||||||
metadata.addAll(tracks)
|
metadata.addAll(tracks)
|
||||||
datasources.addMediaSources(sources)
|
datasources.addMediaSources(sources)
|
||||||
|
|
||||||
|
cast?.addToQueue(tracks)
|
||||||
|
|
||||||
persist()
|
persist()
|
||||||
|
|
||||||
EventBus.send(Event.QueueChanged)
|
EventBus.send(Event.QueueChanged)
|
||||||
|
@ -120,25 +124,29 @@ class QueueManager(val context: Context) {
|
||||||
move(metadata.indexOf(track), current + 1)
|
move(metadata.indexOf(track), current + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cast?.insertNext(track, current)
|
||||||
|
|
||||||
persist()
|
persist()
|
||||||
|
|
||||||
EventBus.send(Event.QueueChanged)
|
EventBus.send(Event.QueueChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(track: Track) {
|
fun remove(track: Track) {
|
||||||
metadata.indexOf(track).let {
|
metadata.indexOf(track).let { trackIndex ->
|
||||||
if (it < 0) {
|
if (trackIndex < 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
datasources.removeMediaSource(it)
|
datasources.removeMediaSource(trackIndex)
|
||||||
metadata.removeAt(it)
|
metadata.removeAt(trackIndex)
|
||||||
|
|
||||||
if (it == current) {
|
cast?.remove(trackIndex)
|
||||||
|
|
||||||
|
if (trackIndex == current) {
|
||||||
CommandBus.send(Command.NextTrack)
|
CommandBus.send(Command.NextTrack)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (it < current) {
|
if (trackIndex < current) {
|
||||||
current--
|
current--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,6 +164,8 @@ class QueueManager(val context: Context) {
|
||||||
datasources.moveMediaSource(oldPosition, newPosition)
|
datasources.moveMediaSource(oldPosition, newPosition)
|
||||||
metadata.add(newPosition, metadata.removeAt(oldPosition))
|
metadata.add(newPosition, metadata.removeAt(oldPosition))
|
||||||
|
|
||||||
|
cast?.move(oldPosition, newPosition)
|
||||||
|
|
||||||
persist()
|
persist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,6 @@ sealed class Response {
|
||||||
object EventBus {
|
object EventBus {
|
||||||
fun send(event: Event) {
|
fun send(event: Event) {
|
||||||
GlobalScope.launch(IO) {
|
GlobalScope.launch(IO) {
|
||||||
Otter.get().eventBus.log()
|
|
||||||
Otter.get().eventBus.offer(event)
|
Otter.get().eventBus.offer(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.github.apognu.otter.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.android.exoplayer2.Player
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||||
|
|
||||||
|
interface CastInterface {
|
||||||
|
fun isCastSessionAvailable(): Boolean = false
|
||||||
|
fun getPlayer(context: Context): Player = SimpleExoPlayer.Builder(context).build()
|
||||||
|
|
||||||
|
fun replaceQueue(tracks: List<Track>) {}
|
||||||
|
fun addToQueue(tracks: List<Track>) {}
|
||||||
|
fun insertNext(track: Track, current: Int) {}
|
||||||
|
fun remove(index: Int) {}
|
||||||
|
fun move(oldPosition: Int, newPosition: Int) {}
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.fragments.BrowseFragment
|
import com.github.apognu.otter.fragments.BrowseFragment
|
||||||
import com.github.apognu.otter.repositories.Repository
|
import com.github.apognu.otter.repositories.Repository
|
||||||
import com.github.kittinunf.fuel.core.Request
|
import com.github.kittinunf.fuel.core.Request
|
||||||
|
import com.google.android.exoplayer2.Player
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||||
import com.google.android.exoplayer2.offline.Download
|
import com.google.android.exoplayer2.offline.Download
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
|
@ -77,3 +79,8 @@ fun Request.authorize(): Request {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Download.getMetadata(): DownloadInfo? = Gson().fromJson(String(this.request.data), DownloadInfo::class.java)
|
fun Download.getMetadata(): DownloadInfo? = Gson().fromJson(String(this.request.data), DownloadInfo::class.java)
|
||||||
|
|
||||||
|
fun Player.onLocal(): SimpleExoPlayer? {
|
||||||
|
return if (this is SimpleExoPlayer) this
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
|
@ -4,9 +4,12 @@ import com.google.android.exoplayer2.offline.Download
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
|
|
||||||
data class User(
|
data class User(
|
||||||
val full_username: String
|
val full_username: String,
|
||||||
|
val tokens: UserTokens
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class UserTokens(val listen: String)
|
||||||
|
|
||||||
sealed class CacheItem<D : Any>(val data: List<D>)
|
sealed class CacheItem<D : Any>(val data: List<D>)
|
||||||
class ArtistsCache(data: List<Artist>) : CacheItem<Artist>(data)
|
class ArtistsCache(data: List<Artist>) : CacheItem<Artist>(data)
|
||||||
class AlbumsCache(data: List<Album>) : CacheItem<Album>(data)
|
class AlbumsCache(data: List<Album>) : CacheItem<Album>(data)
|
||||||
|
|
|
@ -20,6 +20,7 @@ object Userinfo {
|
||||||
|
|
||||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
|
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
|
||||||
setString("actor_username", user.full_username)
|
setString("actor_username", user.full_username)
|
||||||
|
setString("listen_token", user.tokens.listen)
|
||||||
}
|
}
|
||||||
|
|
||||||
user
|
user
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
<style name="AppTheme" parent="AppTheme.Base"></style>
|
||||||
|
|
||||||
|
<style name="AppTheme.Base" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
<item name="android:forceDarkAllowed" tools:targetApi="q">true</item>
|
<item name="android:forceDarkAllowed" tools:targetApi="q">true</item>
|
||||||
|
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
@ -16,8 +18,6 @@
|
||||||
|
|
||||||
<item name="buttonBarNegativeButtonStyle">@style/AppTheme.DialogButtonStyle</item>
|
<item name="buttonBarNegativeButtonStyle">@style/AppTheme.DialogButtonStyle</item>
|
||||||
<item name="buttonBarPositiveButtonStyle">@style/AppTheme.DialogButtonStyle</item>
|
<item name="buttonBarPositiveButtonStyle">@style/AppTheme.DialogButtonStyle</item>
|
||||||
|
|
||||||
<item name="mediaRouteTheme">@style/AppTheme.MediaRouteTheme</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.Fragment"></style>
|
<style name="AppTheme.Fragment"></style>
|
||||||
|
@ -101,12 +101,4 @@
|
||||||
<item name="android:textColor">@android:color/white</item>
|
<item name="android:textColor">@android:color/white</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.MediaRouteTheme" parent="Theme.MediaRouter">
|
|
||||||
<item name="mediaRouteButtonStyle">@style/AppTheme.MediaRouteTheme.ButtonStyle</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="AppTheme.MediaRouteTheme.ButtonStyle" parent="Widget.MediaRouter.Light.MediaRouteButton">
|
|
||||||
<item name="mediaRouteButtonTint">@android:color/white</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue