Antoine POPINEAU
4 years ago
26 changed files with 403 additions and 34 deletions
@ -0,0 +1,45 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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