Added experimental radios support. Fixed linter and fastlane metadata.
parent
3fb0bb55a4
commit
fd1741ca53
@ -0,0 +1,2 @@
|
||||
[*.{kt,kts}]
|
||||
indent_size=2
|
@ -0,0 +1,45 @@
|
||||
package com.github.apognu.otter.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.FunkwhaleAdapter
|
||||
import com.github.apognu.otter.utils.Radio
|
||||
import kotlinx.android.synthetic.main.row_radio.view.*
|
||||
|
||||
class RadiosAdapter(val context: Context?, private val listener: OnRadioClickListener) : FunkwhaleAdapter<Radio, RadiosAdapter.ViewHolder>() {
|
||||
interface OnRadioClickListener {
|
||||
fun onClick(holder: View?, radio: Radio)
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
|
||||
override fun getItemId(position: Int) = data[position].id.toLong()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RadiosAdapter.ViewHolder {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.row_radio, parent, false)
|
||||
|
||||
return ViewHolder(view, listener).also {
|
||||
view.setOnClickListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RadiosAdapter.ViewHolder, position: Int) {
|
||||
val radio = data[position]
|
||||
|
||||
holder.name.text = radio.name
|
||||
holder.description.text = radio.description
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View, val listener: OnRadioClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener {
|
||||
val name = view.name
|
||||
val description = view.description
|
||||
|
||||
override fun onClick(view: View?) {
|
||||
listener.onClick(view, data[layoutPosition])
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package com.github.apognu.otter.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.RadiosAdapter
|
||||
import com.github.apognu.otter.repositories.RadiosRepository
|
||||
import com.github.apognu.otter.utils.Command
|
||||
import com.github.apognu.otter.utils.CommandBus
|
||||
import com.github.apognu.otter.utils.Radio
|
||||
import kotlinx.android.synthetic.main.fragment_radios.*
|
||||
|
||||
class RadiosFragment : FunkwhaleFragment<Radio, RadiosAdapter>() {
|
||||
override val viewRes = R.layout.fragment_radios
|
||||
override val recycler: RecyclerView get() = radios
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
adapter = RadiosAdapter(context, RadioClickListener())
|
||||
repository = RadiosRepository(context)
|
||||
}
|
||||
|
||||
inner class RadioClickListener : RadiosAdapter.OnRadioClickListener {
|
||||
override fun onClick(holder: View?, radio: Radio) {
|
||||
CommandBus.send(Command.PlayRadio(radio))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package com.github.apognu.otter.playback
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
data class RadioSessionBody(val radio_type: String, val custom_radio: Int)
|
||||
data class RadioSession(val id: Int)
|
||||
data class RadioTrackBody(val session: Int)
|
||||
data class RadioTrack(val position: Int, val track: RadioTrackID)
|
||||
data class RadioTrackID(val id: Int)
|
||||
|
||||
class RadioPlayer(val context: Context) {
|
||||
val lock = Semaphore(1)
|
||||
|
||||
private var currentRadio: Radio? = null
|
||||
private var session: Int? = null
|
||||
|
||||
fun play(radio: Radio) {
|
||||
currentRadio = radio
|
||||
session = null
|
||||
|
||||
GlobalScope.launch(IO) {
|
||||
createSession()
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
currentRadio = null
|
||||
session = null
|
||||
}
|
||||
|
||||
fun isActive() = currentRadio != null && session != null
|
||||
|
||||
private suspend fun createSession() {
|
||||
currentRadio?.let { radio ->
|
||||
try {
|
||||
val body = Gson().toJson(RadioSessionBody("custom", radio.id))
|
||||
val result = Fuel.post(mustNormalizeUrl("/api/v1/radios/sessions/"))
|
||||
.authorize()
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body)
|
||||
.awaitObjectResult(gsonDeserializerOf(RadioSession::class.java))
|
||||
|
||||
session = result.get().id
|
||||
|
||||
prepareNextTrack(true)
|
||||
} catch (e: Exception) {
|
||||
withContext(Main) {
|
||||
context.toast(context.getString(R.string.radio_playback_error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun prepareNextTrack(first: Boolean = false) {
|
||||
session?.let { session ->
|
||||
try {
|
||||
val body = Gson().toJson(RadioTrackBody(session))
|
||||
val result = Fuel.post(mustNormalizeUrl("/api/v1/radios/tracks/"))
|
||||
.authorize()
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body)
|
||||
.awaitObjectResult(gsonDeserializerOf(RadioTrack::class.java))
|
||||
|
||||
val track = Fuel.get(mustNormalizeUrl("/api/v1/tracks/${result.get().track.id}/"))
|
||||
.authorize()
|
||||
.awaitObjectResult(gsonDeserializerOf(Track::class.java))
|
||||
|
||||
if (first) {
|
||||
CommandBus.send(Command.ReplaceQueue(listOf(track.get()), true))
|
||||
} else {
|
||||
CommandBus.send(Command.AddToQueue(listOf(track.get())))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Main) {
|
||||
context.toast(context.getString(R.string.radio_playback_error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.utils.FunkwhaleResponse
|
||||
import com.github.apognu.otter.utils.Radio
|
||||
import com.github.apognu.otter.utils.RadiosCache
|
||||
import com.github.apognu.otter.utils.RadiosResponse
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.BufferedReader
|
||||
|
||||
class RadiosRepository(override val context: Context?) : Repository<Radio, RadiosCache>() {
|
||||
override val cacheId = "radios"
|
||||
override val upstream = HttpUpstream<Radio, FunkwhaleResponse<Radio>>(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/", object : TypeToken<RadiosResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Radio>) = RadiosCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(RadiosCache::class.java).deserialize(reader)
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swiper"
|
||||
style="@style/AppTheme.Fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/radios"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
tools:itemCount="10"
|
||||
tools:listitem="@layout/row_playlist" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
style="@style/AppTheme.Title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/radios" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:transitionGroup="true"
|
||||
tools:showIn="@layout/fragment_radios">
|
||||
|
||||
<com.github.apognu.otter.views.SquareImageView
|
||||
android:id="@+id/art"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/cover"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/name"
|
||||
style="@style/AppTheme.ItemTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
tools:text="Hard Rock" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
tools:text="Lorem ipsum dolor sit amet" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
@ -1,5 +1,6 @@
|
||||
#Sat May 30 20:25:10 CEST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.3-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||
|
Loading…
Reference in New Issue