Merge branch 'housekeeping/migrate-to-viewbinding' into 'develop'

Housekeeping/migrate to viewbinding

Closes #67

See merge request funkwhale/funkwhale-android!36
This commit is contained in:
Ryan Harg 2021-07-16 08:03:52 +00:00
commit 06aae36551
35 changed files with 1422 additions and 766 deletions

View File

@ -5,7 +5,6 @@ import java.util.Properties
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
id("kotlin-android-extensions")
id("org.jlleitschuh.gradle.ktlint") version "8.1.0" id("org.jlleitschuh.gradle.ktlint") version "8.1.0"
id("com.gladed.androidgitversion") version "0.4.14" id("com.gladed.androidgitversion") version "0.4.14"
@ -34,6 +33,10 @@ android {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
} }
buildFeatures {
viewBinding = true
}
buildToolsVersion = "29.0.3" buildToolsVersion = "29.0.3"
compileSdkVersion(29) compileSdkVersion(29)

View File

@ -4,36 +4,38 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import audio.funkwhale.ffa.adapters.DownloadsAdapter
import audio.funkwhale.ffa.databinding.ActivityDownloadsBinding
import audio.funkwhale.ffa.utils.Event
import audio.funkwhale.ffa.utils.EventBus
import audio.funkwhale.ffa.utils.getMetadata
import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.Download
import kotlinx.android.synthetic.main.activity_downloads.downloads
import kotlinx.coroutines.Dispatchers.Default import kotlinx.coroutines.Dispatchers.Default
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.DownloadsAdapter
import audio.funkwhale.ffa.utils.Event
import audio.funkwhale.ffa.utils.EventBus
import audio.funkwhale.ffa.utils.getMetadata
class DownloadsActivity : AppCompatActivity() { class DownloadsActivity : AppCompatActivity() {
lateinit var adapter: DownloadsAdapter
private lateinit var adapter: DownloadsAdapter
private lateinit var binding: ActivityDownloadsBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_downloads) binding = ActivityDownloadsBinding.inflate(layoutInflater)
downloads.itemAnimator = null setContentView(binding.root)
adapter = DownloadsAdapter(this, DownloadChangedListener()).also { binding.downloads.itemAnimator = null
adapter = DownloadsAdapter(layoutInflater, this, DownloadChangedListener()).also {
it.setHasStableIds(true) it.setHasStableIds(true)
downloads.layoutManager = LinearLayoutManager(this) binding.downloads.layoutManager = LinearLayoutManager(this)
downloads.adapter = it binding.downloads.adapter = it
} }
lifecycleScope.launch(Default) { lifecycleScope.launch(Default) {
@ -80,17 +82,18 @@ class DownloadsActivity : AppCompatActivity() {
private suspend fun refreshTrack(download: Download) { private suspend fun refreshTrack(download: Download) {
download.getMetadata()?.let { info -> download.getMetadata()?.let { info ->
adapter.downloads.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match -> adapter.downloads.withIndex().associate { it.value to it.index }
if (download.state != info.download?.state) { .filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
withContext(Main) { if (download.state != info.download?.state) {
adapter.downloads[match.second] = info.apply { withContext(Main) {
this.download = download adapter.downloads[match.second] = info.apply {
} this.download = download
}
adapter.notifyItemChanged(match.second) adapter.notifyItemChanged(match.second)
}
} }
} }
}
} }
} }
@ -101,17 +104,18 @@ class DownloadsActivity : AppCompatActivity() {
val download = cursor.download val download = cursor.download
download.getMetadata()?.let { info -> download.getMetadata()?.let { info ->
adapter.downloads.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match -> adapter.downloads.withIndex().associate { it.value to it.index }
if (download.state == Download.STATE_DOWNLOADING && download.percentDownloaded != info.download?.percentDownloaded ?: 0) { .filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
withContext(Main) { if (download.state == Download.STATE_DOWNLOADING && download.percentDownloaded != info.download?.percentDownloaded ?: 0) {
adapter.downloads[match.second] = info.apply { withContext(Main) {
this.download = download adapter.downloads[match.second] = info.apply {
} this.download = download
}
adapter.notifyItemChanged(match.second) adapter.notifyItemChanged(match.second)
}
} }
} }
}
} }
} }
} }

View File

@ -3,17 +3,18 @@ package audio.funkwhale.ffa.activities
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.databinding.ActivityLicencesBinding
import kotlinx.android.synthetic.main.activity_licences.* import audio.funkwhale.ffa.databinding.RowLicenceBinding
import kotlinx.android.synthetic.main.row_licence.view.*
class LicencesActivity : AppCompatActivity() { class LicencesActivity : AppCompatActivity() {
private lateinit var binding: ActivityLicencesBinding
data class Licence(val name: String, val licence: String, val url: String) data class Licence(val name: String, val licence: String, val url: String)
interface OnLicenceClickListener { interface OnLicenceClickListener {
@ -23,15 +24,18 @@ class LicencesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_licences) binding = ActivityLicencesBinding.inflate(layoutInflater)
setContentView(binding.root)
LicencesAdapter(OnLicenceClick()).also { LicencesAdapter(OnLicenceClick()).also {
licences.layoutManager = LinearLayoutManager(this) binding.licences.layoutManager = LinearLayoutManager(this)
licences.adapter = it binding.licences.adapter = it
} }
} }
private inner class LicencesAdapter(val listener: OnLicenceClickListener) : RecyclerView.Adapter<LicencesAdapter.ViewHolder>() { private inner class LicencesAdapter(val listener: OnLicenceClickListener) :
RecyclerView.Adapter<LicencesAdapter.ViewHolder>() {
val licences = listOf( val licences = listOf(
Licence( Licence(
"ExoPlayer", "ExoPlayer",
@ -73,10 +77,9 @@ class LicencesActivity : AppCompatActivity() {
override fun getItemCount() = licences.size override fun getItemCount() = licences.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(this@LicencesActivity).inflate(R.layout.row_licence, parent, false) val binding = RowLicenceBinding.inflate(layoutInflater)
return ViewHolder(binding).also {
return ViewHolder(view).also { binding.root.setOnClickListener(it)
view.setOnClickListener(it)
} }
} }
@ -87,9 +90,10 @@ class LicencesActivity : AppCompatActivity() {
holder.licence.text = item.licence holder.licence.text = item.licence
} }
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener { inner class ViewHolder(binding: RowLicenceBinding) : RecyclerView.ViewHolder(binding.root),
val name = view.name View.OnClickListener {
val licence = view.licence val name = binding.name
val licence = binding.licence
override fun onClick(view: View?) { override fun onClick(view: View?) {
listener.onClick(licences[layoutPosition].url) listener.onClick(licences[layoutPosition].url)

View File

@ -10,6 +10,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.doOnLayout import androidx.core.view.doOnLayout
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.ActivityLoginBinding
import audio.funkwhale.ffa.fragments.LoginDialog import audio.funkwhale.ffa.fragments.LoginDialog
import audio.funkwhale.ffa.utils.AppContext import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.Userinfo import audio.funkwhale.ffa.utils.Userinfo
@ -19,17 +20,21 @@ import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.github.kittinunf.result.Result import com.github.kittinunf.result.Result
import com.google.gson.Gson import com.google.gson.Gson
import com.preference.PowerPreference import com.preference.PowerPreference
import kotlinx.android.synthetic.main.activity_login.*
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
data class FwCredentials(val token: String, val non_field_errors: List<String>?) data class FwCredentials(val token: String, val non_field_errors: List<String>?)
class LoginActivity : AppCompatActivity() { class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login) binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
limitContainerWidth() limitContainerWidth()
} }
@ -37,40 +42,40 @@ class LoginActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
anonymous?.setOnCheckedChangeListener { _, isChecked -> binding.anonymous.setOnCheckedChangeListener { _, isChecked ->
val state = when (isChecked) { val state = when (isChecked) {
true -> View.GONE true -> View.GONE
false -> View.VISIBLE false -> View.VISIBLE
} }
username_field.visibility = state binding.usernameField.visibility = state
password_field.visibility = state binding.passwordField.visibility = state
} }
login?.setOnClickListener { binding.login?.setOnClickListener {
var hostname = hostname.text.toString().trim() var hostname = binding.hostname.text.toString().trim()
val username = username.text.toString() val username = binding.username.text.toString()
val password = password.text.toString() val password = binding.password.text.toString()
try { try {
if (hostname.isEmpty()) throw Exception(getString(R.string.login_error_hostname)) if (hostname.isEmpty()) throw Exception(getString(R.string.login_error_hostname))
Uri.parse(hostname).apply { Uri.parse(hostname).apply {
if (!cleartext.isChecked && scheme == "http") { if (!binding.cleartext.isChecked && scheme == "http") {
throw Exception(getString(R.string.login_error_hostname_https)) throw Exception(getString(R.string.login_error_hostname_https))
} }
if (scheme == null) { if (scheme == null) {
hostname = when (cleartext.isChecked) { hostname = when (binding.cleartext.isChecked) {
true -> "http://$hostname" true -> "http://$hostname"
false -> "https://$hostname" false -> "https://$hostname"
} }
} }
} }
hostname_field.error = "" binding.hostnameField.error = ""
when (anonymous.isChecked) { when (binding.anonymous.isChecked) {
false -> authedLogin(hostname, username, password) false -> authedLogin(hostname, username, password)
true -> anonymousLogin(hostname) true -> anonymousLogin(hostname)
} }
@ -79,7 +84,7 @@ class LoginActivity : AppCompatActivity() {
if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname) if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
else e.message else e.message
hostname_field.error = message binding.hostnameField.error = message
} }
} }
} }
@ -130,13 +135,13 @@ class LoginActivity : AppCompatActivity() {
val error = Gson().fromJson(String(response.data), FwCredentials::class.java) val error = Gson().fromJson(String(response.data), FwCredentials::class.java)
hostname_field.error = null binding.hostnameField.error = null
username_field.error = null binding.usernameField.error = null
if (error != null && error.non_field_errors?.isNotEmpty() == true) { if (error != null && error.non_field_errors?.isNotEmpty() == true) {
username_field.error = error.non_field_errors[0] binding.usernameField.error = error.non_field_errors[0]
} else { } else {
hostname_field.error = result.error.localizedMessage binding.hostnameField.error = result.error.localizedMessage
} }
} }
} }
@ -147,7 +152,7 @@ class LoginActivity : AppCompatActivity() {
if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname) if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
else e.message else e.message
hostname_field.error = message binding.hostnameField.error = message
} }
} }
} }
@ -177,7 +182,7 @@ class LoginActivity : AppCompatActivity() {
is Result.Failure -> { is Result.Failure -> {
dialog.dismiss() dialog.dismiss()
hostname_field.error = result.error.localizedMessage binding.hostnameField.error = result.error.localizedMessage
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -187,20 +192,20 @@ class LoginActivity : AppCompatActivity() {
if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname) if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
else e.message else e.message
hostname_field.error = message binding.hostnameField.error = message
} }
} }
} }
private fun limitContainerWidth() { private fun limitContainerWidth() {
container.doOnLayout { binding.container.doOnLayout {
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && container.width >= 1440) { if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && binding.container.width >= 1440) {
container.layoutParams.width = 1440 binding.container.layoutParams.width = 1440
} else { } else {
container.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT binding.container.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
} }
container.requestLayout() binding.container.requestLayout()
} }
} }
} }

View File

@ -21,24 +21,8 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitStringResponse
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.offline.DownloadService
import com.google.gson.Gson
import com.preference.PowerPreference
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.partial_now_playing.*
import kotlinx.coroutines.Dispatchers.Default
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.ActivityMainBinding
import audio.funkwhale.ffa.fragments.* import audio.funkwhale.ffa.fragments.*
import audio.funkwhale.ffa.playback.MediaControlsManager import audio.funkwhale.ffa.playback.MediaControlsManager
import audio.funkwhale.ffa.playback.PinService import audio.funkwhale.ffa.playback.PinService
@ -48,6 +32,20 @@ import audio.funkwhale.ffa.repositories.FavoritesRepository
import audio.funkwhale.ffa.repositories.Repository import audio.funkwhale.ffa.repositories.Repository
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.utils.*
import audio.funkwhale.ffa.views.DisableableFrameLayout import audio.funkwhale.ffa.views.DisableableFrameLayout
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitStringResponse
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.offline.DownloadService
import com.google.gson.Gson
import com.preference.PowerPreference
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.coroutines.Dispatchers.Default
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
enum class ResultCode(val code: Int) { enum class ResultCode(val code: Int) {
@ -58,13 +56,18 @@ class MainActivity : AppCompatActivity() {
private val favoritedRepository = FavoritedRepository(this) private val favoritedRepository = FavoritedRepository(this)
private var menu: Menu? = null private var menu: Menu? = null
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AppContext.init(this) AppContext.init(this)
setContentView(R.layout.activity_main) binding = ActivityMainBinding.inflate(layoutInflater)
setSupportActionBar(appbar)
setContentView(binding.root)
setSupportActionBar(binding.appbar)
when (intent.action) { when (intent.action) {
MediaControlsManager.NOTIFICATION_ACTION_OPEN_QUEUE.toString() -> launchDialog(QueueFragment()) MediaControlsManager.NOTIFICATION_ACTION_OPEN_QUEUE.toString() -> launchDialog(QueueFragment())
@ -81,9 +84,9 @@ class MainActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(container as? DisableableFrameLayout)?.setShouldRegisterTouch { _ -> (binding.container as? DisableableFrameLayout)?.setShouldRegisterTouch { _ ->
if (now_playing.isOpened()) { if (binding.nowPlaying.isOpened()) {
now_playing.close() binding.nowPlaying.close()
return@setShouldRegisterTouch false return@setShouldRegisterTouch false
} }
@ -102,48 +105,51 @@ class MainActivity : AppCompatActivity() {
Userinfo.get() Userinfo.get()
} }
now_playing_toggle.setOnClickListener { with(binding) {
CommandBus.send(Command.ToggleState)
}
now_playing_next.setOnClickListener { nowPlayingContainer?.nowPlayingToggle?.setOnClickListener {
CommandBus.send(Command.NextTrack) CommandBus.send(Command.ToggleState)
}
now_playing_details_previous.setOnClickListener {
CommandBus.send(Command.PreviousTrack)
}
now_playing_details_next.setOnClickListener {
CommandBus.send(Command.NextTrack)
}
now_playing_details_toggle.setOnClickListener {
CommandBus.send(Command.ToggleState)
}
now_playing_details_progress.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener {
override fun onStopTrackingTouch(view: SeekBar?) {}
override fun onStartTrackingTouch(view: SeekBar?) {}
override fun onProgressChanged(view: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
CommandBus.send(Command.Seek(progress))
}
} }
})
landscape_queue?.let { nowPlayingContainer?.nowPlayingNext?.setOnClickListener {
supportFragmentManager.beginTransaction() CommandBus.send(Command.NextTrack)
.replace(R.id.landscape_queue, LandscapeQueueFragment()).commit() }
nowPlayingContainer?.nowPlayingDetailsPrevious?.setOnClickListener {
CommandBus.send(Command.PreviousTrack)
}
nowPlayingContainer?.nowPlayingDetailsNext?.setOnClickListener {
CommandBus.send(Command.NextTrack)
}
nowPlayingContainer?.nowPlayingDetailsToggle?.setOnClickListener {
CommandBus.send(Command.ToggleState)
}
binding.nowPlayingContainer?.nowPlayingDetailsProgress?.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener {
override fun onStopTrackingTouch(view: SeekBar?) {}
override fun onStartTrackingTouch(view: SeekBar?) {}
override fun onProgressChanged(view: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
CommandBus.send(Command.Seek(progress))
}
}
})
landscapeQueue?.let {
supportFragmentManager.beginTransaction()
.replace(R.id.landscape_queue, LandscapeQueueFragment()).commit()
}
} }
} }
override fun onBackPressed() { override fun onBackPressed() {
if (now_playing.isOpened()) { if (binding.nowPlaying.isOpened()) {
now_playing.close() binding.nowPlaying.close()
return return
} }
@ -173,7 +179,7 @@ class MainActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
now_playing.close() binding.nowPlaying.close()
(supportFragmentManager.fragments.last() as? BrowseFragment)?.let { (supportFragmentManager.fragments.last() as? BrowseFragment)?.let {
it.selectTabAt(0) it.selectTabAt(0)
@ -305,29 +311,29 @@ class MainActivity : AppCompatActivity() {
is Event.Buffering -> { is Event.Buffering -> {
when (message.value) { when (message.value) {
true -> now_playing_buffering.visibility = View.VISIBLE true -> binding.nowPlayingContainer?.nowPlayingBuffering?.visibility = View.VISIBLE
false -> now_playing_buffering.visibility = View.GONE false -> binding.nowPlayingContainer?.nowPlayingBuffering?.visibility = View.GONE
} }
} }
is Event.PlaybackStopped -> { is Event.PlaybackStopped -> {
if (now_playing.visibility == View.VISIBLE) { if (binding.nowPlaying.visibility == View.VISIBLE) {
(container.layoutParams as? ViewGroup.MarginLayoutParams)?.let { (binding.container.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
it.bottomMargin = it.bottomMargin / 2 it.bottomMargin = it.bottomMargin / 2
} }
landscape_queue?.let { landscape_queue -> binding.landscapeQueue?.let { landscape_queue ->
(landscape_queue.layoutParams as? ViewGroup.MarginLayoutParams)?.let { (landscape_queue.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
it.bottomMargin = it.bottomMargin / 2 it.bottomMargin = it.bottomMargin / 2
} }
} }
now_playing.animate() binding.nowPlaying.animate()
.alpha(0.0f) .alpha(0.0f)
.setDuration(400) .setDuration(400)
.setListener(object : AnimatorListenerAdapter() { .setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animator: Animator?) { override fun onAnimationEnd(animator: Animator?) {
now_playing.visibility = View.GONE binding.nowPlaying.visibility = View.GONE
} }
}) })
.start() .start()
@ -339,13 +345,14 @@ class MainActivity : AppCompatActivity() {
is Event.StateChanged -> { is Event.StateChanged -> {
when (message.playing) { when (message.playing) {
true -> { true -> {
now_playing_toggle.icon = getDrawable(R.drawable.pause) binding.nowPlayingContainer?.nowPlayingToggle?.icon = getDrawable(R.drawable.pause)
now_playing_details_toggle.icon = getDrawable(R.drawable.pause) binding.nowPlayingContainer?.nowPlayingToggle?.icon = getDrawable(R.drawable.pause)
} }
false -> { false -> {
now_playing_toggle.icon = getDrawable(R.drawable.play) binding.nowPlayingContainer?.nowPlayingToggle?.icon = getDrawable(R.drawable.play)
now_playing_details_toggle.icon = getDrawable(R.drawable.play) binding.nowPlayingContainer?.nowPlayingDetailsToggle?.icon =
getDrawable(R.drawable.play)
} }
} }
} }
@ -369,9 +376,13 @@ class MainActivity : AppCompatActivity() {
is Command.StartService -> { is Command.StartService -> {
Build.VERSION_CODES.O.onApi( Build.VERSION_CODES.O.onApi(
{ {
startForegroundService(Intent(this@MainActivity, PlayerService::class.java).apply { startForegroundService(
putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString()) Intent(
}) this@MainActivity,
PlayerService::class.java
).apply {
putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString())
})
}, },
{ {
startService(Intent(this@MainActivity, PlayerService::class.java).apply { startService(Intent(this@MainActivity, PlayerService::class.java).apply {
@ -384,7 +395,12 @@ class MainActivity : AppCompatActivity() {
is Command.RefreshTrack -> refreshCurrentTrack(command.track) is Command.RefreshTrack -> refreshCurrentTrack(command.track)
is Command.AddToPlaylist -> if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { is Command.AddToPlaylist -> if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
AddToPlaylistDialog.show(this@MainActivity, lifecycleScope, command.tracks) AddToPlaylistDialog.show(
layoutInflater,
this@MainActivity,
lifecycleScope,
command.tracks
)
} }
} }
} }
@ -392,8 +408,8 @@ class MainActivity : AppCompatActivity() {
lifecycleScope.launch(Main) { lifecycleScope.launch(Main) {
ProgressBus.get().collect { (current, duration, percent) -> ProgressBus.get().collect { (current, duration, percent) ->
now_playing_progress.progress = percent binding.nowPlayingContainer?.nowPlayingProgress?.progress = percent
now_playing_details_progress.progress = percent binding.nowPlayingContainer?.nowPlayingDetailsProgress?.progress = percent
val currentMins = (current / 1000) / 60 val currentMins = (current / 1000) / 60
val currentSecs = (current / 1000) % 60 val currentSecs = (current / 1000) % 60
@ -401,59 +417,61 @@ class MainActivity : AppCompatActivity() {
val durationMins = duration / 60 val durationMins = duration / 60
val durationSecs = duration % 60 val durationSecs = duration % 60
now_playing_details_progress_current.text = "%02d:%02d".format(currentMins, currentSecs) binding.nowPlayingContainer?.nowPlayingDetailsProgressCurrent?.text =
now_playing_details_progress_duration.text = "%02d:%02d".format(durationMins, durationSecs) "%02d:%02d".format(currentMins, currentSecs)
binding.nowPlayingContainer?.nowPlayingDetailsProgressDuration?.text =
"%02d:%02d".format(durationMins, durationSecs)
} }
} }
} }
private fun refreshCurrentTrack(track: Track?) { private fun refreshCurrentTrack(track: Track?) {
track?.let { track?.let {
if (now_playing.visibility == View.GONE) { if (binding.nowPlaying.visibility == View.GONE) {
now_playing.visibility = View.VISIBLE binding.nowPlaying.visibility = View.VISIBLE
now_playing.alpha = 0f binding.nowPlaying.alpha = 0f
now_playing.animate() binding.nowPlaying.animate()
.alpha(1.0f) .alpha(1.0f)
.setDuration(400) .setDuration(400)
.setListener(null) .setListener(null)
.start() .start()
(container.layoutParams as? ViewGroup.MarginLayoutParams)?.let { (binding.container.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
it.bottomMargin = it.bottomMargin * 2 it.bottomMargin = it.bottomMargin * 2
} }
landscape_queue?.let { landscape_queue -> binding.landscapeQueue?.let { landscape_queue ->
(landscape_queue.layoutParams as? ViewGroup.MarginLayoutParams)?.let { (landscape_queue.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
it.bottomMargin = it.bottomMargin * 2 it.bottomMargin = it.bottomMargin * 2
} }
} }
} }
now_playing_title.text = track.title binding.nowPlayingContainer?.nowPlayingTitle?.text = track.title
now_playing_album.text = track.artist.name binding.nowPlayingContainer?.nowPlayingAlbum?.text = track.artist.name
now_playing_toggle.icon = getDrawable(R.drawable.pause) binding.nowPlayingContainer?.nowPlayingToggle?.icon = getDrawable(R.drawable.pause)
now_playing_details_title.text = track.title binding.nowPlayingContainer?.nowPlayingDetailsTitle?.text = track.title
now_playing_details_artist.text = track.artist.name binding.nowPlayingContainer?.nowPlayingDetailsArtist?.text = track.artist.name
now_playing_details_toggle.icon = getDrawable(R.drawable.pause) binding.nowPlayingContainer?.nowPlayingDetailsToggle?.icon = getDrawable(R.drawable.pause)
Picasso.get() Picasso.get()
.maybeLoad(maybeNormalizeUrl(track.album?.cover?.urls?.original)) .maybeLoad(maybeNormalizeUrl(track.album?.cover?.urls?.original))
.fit() .fit()
.centerCrop() .centerCrop()
.into(now_playing_cover) .into(binding.nowPlayingContainer?.nowPlayingCover)
now_playing_details_cover?.let { now_playing_details_cover -> binding.nowPlayingContainer?.nowPlayingDetailsCover?.let { nowPlayingDetailsCover ->
Picasso.get() Picasso.get()
.maybeLoad(maybeNormalizeUrl(track.album?.cover())) .maybeLoad(maybeNormalizeUrl(track.album?.cover()))
.fit() .fit()
.centerCrop() .centerCrop()
.transform(RoundedCornersTransformation(16, 0)) .transform(RoundedCornersTransformation(16, 0))
.into(now_playing_details_cover) .into(nowPlayingDetailsCover)
} }
if (now_playing_details_cover == null) { if (binding.nowPlayingContainer?.nowPlayingCover == null) {
lifecycleScope.launch(Default) { lifecycleScope.launch(Default) {
val width = DisplayMetrics().apply { val width = DisplayMetrics().apply {
windowManager.defaultDisplay.getMetrics(this) windowManager.defaultDisplay.getMetrics(this)
@ -469,12 +487,12 @@ class MainActivity : AppCompatActivity() {
} }
withContext(Main) { withContext(Main) {
now_playing_details.background = backgroundCover binding.nowPlayingContainer?.nowPlayingDetails?.background = backgroundCover
} }
} }
} }
now_playing_details_repeat?.let { now_playing_details_repeat -> binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.let { now_playing_details_repeat ->
changeRepeatMode(Cache.get(this@MainActivity, "repeat")?.readLine()?.toInt() ?: 0) changeRepeatMode(Cache.get(this@MainActivity, "repeat")?.readLine()?.toInt() ?: 0)
now_playing_details_repeat.setOnClickListener { now_playing_details_repeat.setOnClickListener {
@ -484,11 +502,11 @@ class MainActivity : AppCompatActivity() {
} }
} }
now_playing_details_info?.let { now_playing_details_info -> binding.nowPlayingContainer?.nowPlayingDetailsInfo?.let { nowPlayingDetailsInfo ->
now_playing_details_info.setOnClickListener { nowPlayingDetailsInfo.setOnClickListener {
PopupMenu( PopupMenu(
this@MainActivity, this@MainActivity,
now_playing_details_info, nowPlayingDetailsInfo,
Gravity.START, Gravity.START,
R.attr.actionOverflowMenuStyle, R.attr.actionOverflowMenuStyle,
0 0
@ -507,7 +525,7 @@ class MainActivity : AppCompatActivity() {
.show(supportFragmentManager, "dialog") .show(supportFragmentManager, "dialog")
} }
now_playing.close() binding.nowPlaying.close()
true true
} }
@ -517,7 +535,7 @@ class MainActivity : AppCompatActivity() {
} }
} }
now_playing_details_favorite?.let { now_playing_details_favorite -> binding.nowPlayingContainer?.nowPlayingDetailsFavorite?.let { now_playing_details_favorite ->
favoritedRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _, _ -> favoritedRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _, _ ->
lifecycleScope.launch(Main) { lifecycleScope.launch(Main) {
track.favorite = favorites.contains(track.id) track.favorite = favorites.contains(track.id)
@ -547,7 +565,7 @@ class MainActivity : AppCompatActivity() {
favoriteRepository.fetch(Repository.Origin.Network.origin) favoriteRepository.fetch(Repository.Origin.Network.origin)
} }
now_playing_details_add_to_playlist.setOnClickListener { binding.nowPlayingContainer?.nowPlayingDetailsAddToPlaylist?.setOnClickListener {
CommandBus.send(Command.AddToPlaylist(listOf(track))) CommandBus.send(Command.AddToPlaylist(listOf(track)))
} }
} }
@ -560,14 +578,14 @@ class MainActivity : AppCompatActivity() {
0 -> { 0 -> {
Cache.set(this@MainActivity, "repeat", "0".toByteArray()) Cache.set(this@MainActivity, "repeat", "0".toByteArray())
now_playing_details_repeat?.setImageResource(R.drawable.repeat) binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat)
now_playing_details_repeat?.setColorFilter( binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
ContextCompat.getColor( ContextCompat.getColor(
this, this,
R.color.controlForeground R.color.controlForeground
) )
) )
now_playing_details_repeat?.alpha = 0.2f binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.alpha = 0.2f
CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_OFF)) CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_OFF))
} }
@ -576,14 +594,14 @@ class MainActivity : AppCompatActivity() {
1 -> { 1 -> {
Cache.set(this@MainActivity, "repeat", "1".toByteArray()) Cache.set(this@MainActivity, "repeat", "1".toByteArray())
now_playing_details_repeat?.setImageResource(R.drawable.repeat) binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat)
now_playing_details_repeat?.setColorFilter( binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
ContextCompat.getColor( ContextCompat.getColor(
this, this,
R.color.controlForeground R.color.controlForeground
) )
) )
now_playing_details_repeat?.alpha = 1.0f binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.alpha = 1.0f
CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_ALL)) CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_ALL))
} }
@ -591,14 +609,14 @@ class MainActivity : AppCompatActivity() {
// From repeat one to no repeat // From repeat one to no repeat
2 -> { 2 -> {
Cache.set(this@MainActivity, "repeat", "2".toByteArray()) Cache.set(this@MainActivity, "repeat", "2".toByteArray())
now_playing_details_repeat?.setImageResource(R.drawable.repeat_one) binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat_one)
now_playing_details_repeat?.setColorFilter( binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
ContextCompat.getColor( ContextCompat.getColor(
this, this,
R.color.controlForeground R.color.controlForeground
) )
) )
now_playing_details_repeat?.alpha = 1.0f binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.alpha = 1.0f
CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_ONE)) CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_ONE))
} }

View File

@ -6,15 +6,14 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.SearchAdapter import audio.funkwhale.ffa.adapters.SearchAdapter
import audio.funkwhale.ffa.databinding.ActivitySearchBinding
import audio.funkwhale.ffa.fragments.AddToPlaylistDialog import audio.funkwhale.ffa.fragments.AddToPlaylistDialog
import audio.funkwhale.ffa.fragments.AlbumsFragment import audio.funkwhale.ffa.fragments.AlbumsFragment
import audio.funkwhale.ffa.fragments.ArtistsFragment import audio.funkwhale.ffa.fragments.ArtistsFragment
import audio.funkwhale.ffa.repositories.* import audio.funkwhale.ffa.repositories.*
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.utils.*
import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.Download
import kotlinx.android.synthetic.main.activity_search.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -25,25 +24,28 @@ import java.util.*
class SearchActivity : AppCompatActivity() { class SearchActivity : AppCompatActivity() {
private lateinit var adapter: SearchAdapter private lateinit var adapter: SearchAdapter
lateinit var artistsRepository: ArtistsSearchRepository private lateinit var artistsRepository: ArtistsSearchRepository
lateinit var albumsRepository: AlbumsSearchRepository private lateinit var albumsRepository: AlbumsSearchRepository
lateinit var tracksRepository: TracksSearchRepository private lateinit var tracksRepository: TracksSearchRepository
private lateinit var favoritesRepository: FavoritesRepository
lateinit var favoritesRepository: FavoritesRepository private lateinit var binding: ActivitySearchBinding
var done = 0 var done = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search) binding = ActivitySearchBinding.inflate(layoutInflater)
adapter = SearchAdapter(this, SearchResultClickListener(), FavoriteListener()).also { setContentView(binding.root)
results.layoutManager = LinearLayoutManager(this)
results.adapter = it
}
search.requestFocus() adapter =
SearchAdapter(layoutInflater, this, SearchResultClickListener(), FavoriteListener()).also {
binding.results.layoutManager = LinearLayoutManager(this)
binding.results.adapter = it
}
binding.search.requestFocus()
} }
override fun onResume() { override fun onResume() {
@ -53,7 +55,12 @@ class SearchActivity : AppCompatActivity() {
CommandBus.get().collect { command -> CommandBus.get().collect { command ->
when (command) { when (command) {
is Command.AddToPlaylist -> if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { is Command.AddToPlaylist -> if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
AddToPlaylistDialog.show(this@SearchActivity, lifecycleScope, command.tracks) AddToPlaylistDialog.show(
layoutInflater,
this@SearchActivity,
lifecycleScope,
command.tracks
)
} }
} }
} }
@ -72,9 +79,10 @@ class SearchActivity : AppCompatActivity() {
tracksRepository = TracksSearchRepository(this@SearchActivity, "") tracksRepository = TracksSearchRepository(this@SearchActivity, "")
favoritesRepository = FavoritesRepository(this@SearchActivity) favoritesRepository = FavoritesRepository(this@SearchActivity)
search.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener { binding.search.setOnQueryTextListener(object :
androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(rawQuery: String?): Boolean { override fun onQueryTextSubmit(rawQuery: String?): Boolean {
search.clearFocus() binding.search.clearFocus()
rawQuery?.let { rawQuery?.let {
done = 0 done = 0
@ -85,35 +93,38 @@ class SearchActivity : AppCompatActivity() {
albumsRepository.query = query.toLowerCase(Locale.ROOT) albumsRepository.query = query.toLowerCase(Locale.ROOT)
tracksRepository.query = query.toLowerCase(Locale.ROOT) tracksRepository.query = query.toLowerCase(Locale.ROOT)
search_spinner.visibility = View.VISIBLE binding.searchSpinner.visibility = View.VISIBLE
search_empty.visibility = View.GONE binding.searchEmpty.visibility = View.GONE
search_no_results.visibility = View.GONE binding.searchNoResults.visibility = View.GONE
adapter.artists.clear() adapter.artists.clear()
adapter.albums.clear() adapter.albums.clear()
adapter.tracks.clear() adapter.tracks.clear()
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
artistsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { artists, _, _, _ -> artistsRepository.fetch(Repository.Origin.Network.origin)
done++ .untilNetwork(lifecycleScope) { artists, _, _, _ ->
done++
adapter.artists.addAll(artists) adapter.artists.addAll(artists)
refresh() refresh()
} }
albumsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { albums, _, _, _ -> albumsRepository.fetch(Repository.Origin.Network.origin)
done++ .untilNetwork(lifecycleScope) { albums, _, _, _ ->
done++
adapter.albums.addAll(albums) adapter.albums.addAll(albums)
refresh() refresh()
} }
tracksRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { tracks, _, _, _ -> tracksRepository.fetch(Repository.Origin.Network.origin)
done++ .untilNetwork(lifecycleScope) { tracks, _, _, _ ->
done++
adapter.tracks.addAll(tracks) adapter.tracks.addAll(tracks)
refresh() refresh()
} }
} }
return true return true
@ -127,25 +138,31 @@ class SearchActivity : AppCompatActivity() {
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
if (adapter.artists.size + adapter.albums.size + adapter.tracks.size == 0) { if (adapter.artists.size + adapter.albums.size + adapter.tracks.size == 0) {
search_no_results.visibility = View.VISIBLE binding.searchNoResults.visibility = View.VISIBLE
} else { } else {
search_no_results.visibility = View.GONE binding.searchNoResults.visibility = View.GONE
} }
if (done == 3) { if (done == 3) {
search_spinner.visibility = View.INVISIBLE binding.searchSpinner.visibility = View.INVISIBLE
} }
} }
private suspend fun refreshDownloadedTrack(download: Download) { private suspend fun refreshDownloadedTrack(download: Download) {
if (download.state == Download.STATE_COMPLETED) { if (download.state == Download.STATE_COMPLETED) {
download.getMetadata()?.let { info -> download.getMetadata()?.let { info ->
adapter.tracks.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match -> adapter.tracks.withIndex().associate { it.value to it.index }
withContext(Dispatchers.Main) { .filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
adapter.tracks[match.second].downloaded = true withContext(Dispatchers.Main) {
adapter.notifyItemChanged(adapter.getPositionOf(SearchAdapter.ResultType.Track, match.second)) adapter.tracks[match.second].downloaded = true
adapter.notifyItemChanged(
adapter.getPositionOf(
SearchAdapter.ResultType.Track,
match.second
)
)
}
} }
}
} }
} }
} }

View File

@ -1,6 +1,10 @@
package audio.funkwhale.ffa.activities package audio.funkwhale.ffa.activities
import android.content.* import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@ -10,18 +14,22 @@ import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SeekBarPreference import androidx.preference.SeekBarPreference
import audio.funkwhale.ffa.BuildConfig
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.ActivitySettingsBinding
import audio.funkwhale.ffa.utils.Cache import audio.funkwhale.ffa.utils.Cache
import audio.funkwhale.ffa.utils.Command import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus import audio.funkwhale.ffa.utils.CommandBus
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings) binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
@ -35,7 +43,10 @@ class SettingsActivity : AppCompatActivity() {
fun getThemeResId(): Int = R.style.AppTheme fun getThemeResId(): Int = R.style.AppTheme
} }
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { class SettingsFragment :
PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@ -68,7 +79,11 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
Cache.get(activity, "crashdump")?.readLines()?.joinToString("\n").also { Cache.get(activity, "crashdump")?.readLines()?.joinToString("\n").also {
clip.setPrimaryClip(ClipData.newPlainText("Otter logs", it)) clip.setPrimaryClip(ClipData.newPlainText("Otter logs", it))
Toast.makeText(activity, activity.getString(R.string.settings_crash_report_copied), Toast.LENGTH_SHORT).show() Toast.makeText(
activity,
activity.getString(R.string.settings_crash_report_copied),
Toast.LENGTH_SHORT
).show()
} }
} }
} }
@ -150,7 +165,8 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
} }
preferenceManager.findPreference<Preference>("version")?.let { preferenceManager.findPreference<Preference>("version")?.let {
it.summary = "${audio.funkwhale.ffa.BuildConfig.VERSION_NAME} (${audio.funkwhale.ffa.BuildConfig.VERSION_CODE})" it.summary =
"${audio.funkwhale.ffa.BuildConfig.VERSION_NAME} (${audio.funkwhale.ffa.BuildConfig.VERSION_CODE})"
} }
} }
} }

View File

@ -4,11 +4,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.utils.AppContext import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.Settings import audio.funkwhale.ffa.utils.Settings
class SplashActivity : AppCompatActivity() { class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@ -5,30 +5,36 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.databinding.RowAlbumBinding
import audio.funkwhale.ffa.fragments.OtterAdapter import audio.funkwhale.ffa.fragments.FFAAdapter
import audio.funkwhale.ffa.utils.Album import audio.funkwhale.ffa.utils.Album
import audio.funkwhale.ffa.utils.maybeLoad import audio.funkwhale.ffa.utils.maybeLoad
import audio.funkwhale.ffa.utils.maybeNormalizeUrl import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_album.view.*
import kotlinx.android.synthetic.main.row_artist.view.art
class AlbumsAdapter(val context: Context?, private val listener: OnAlbumClickListener) : OtterAdapter<Album, AlbumsAdapter.ViewHolder>() { class AlbumsAdapter(
val layoutInflater: LayoutInflater,
val context: Context?,
private val listener: OnAlbumClickListener
) : FFAAdapter<Album, AlbumsAdapter.ViewHolder>() {
interface OnAlbumClickListener { interface OnAlbumClickListener {
fun onClick(view: View?, album: Album) fun onClick(view: View?, album: Album)
} }
private lateinit var binding: RowAlbumBinding
override fun getItemId(position: Int): Long = data[position].id.toLong() override fun getItemId(position: Int): Long = data[position].id.toLong()
override fun getItemCount() = data.size override fun getItemCount() = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_album, parent, false)
return ViewHolder(view, listener).also { binding = RowAlbumBinding.inflate(layoutInflater, parent, false)
view.setOnClickListener(it)
return ViewHolder(binding, listener).also {
binding.root.setOnClickListener(it)
} }
} }
@ -43,21 +49,22 @@ class AlbumsAdapter(val context: Context?, private val listener: OnAlbumClickLis
holder.title.text = album.title holder.title.text = album.title
holder.artist.text = album.artist.name holder.artist.text = album.artist.name
holder.release_date.visibility = View.GONE holder.releaseDate.visibility = View.GONE
album.release_date?.split('-')?.getOrNull(0)?.let { year -> album.release_date?.split('-')?.getOrNull(0)?.let { year ->
if (year.isNotEmpty()) { if (year.isNotEmpty()) {
holder.release_date.visibility = View.VISIBLE holder.releaseDate.visibility = View.VISIBLE
holder.release_date.text = year holder.releaseDate.text = year
} }
} }
} }
inner class ViewHolder(view: View, private val listener: OnAlbumClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener { inner class ViewHolder(binding: RowAlbumBinding, private val listener: OnAlbumClickListener) :
val art = view.art RecyclerView.ViewHolder(binding.root), View.OnClickListener {
val title = view.title val art = binding.art
val artist = view.artist val title = binding.title
val release_date = view.release_date val artist = binding.artist
val releaseDate = binding.releaseDate
override fun onClick(view: View?) { override fun onClick(view: View?) {
listener.onClick(view, data[layoutPosition]) listener.onClick(view, data[layoutPosition])

View File

@ -1,20 +1,25 @@
package audio.funkwhale.ffa.adapters package audio.funkwhale.ffa.adapters
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.fragments.OtterAdapter import audio.funkwhale.ffa.databinding.RowAlbumGridBinding
import audio.funkwhale.ffa.fragments.FFAAdapter
import audio.funkwhale.ffa.utils.Album import audio.funkwhale.ffa.utils.Album
import audio.funkwhale.ffa.utils.maybeLoad import audio.funkwhale.ffa.utils.maybeLoad
import audio.funkwhale.ffa.utils.maybeNormalizeUrl import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_album_grid.view.*
class AlbumsGridAdapter(val context: Context?, private val listener: OnAlbumClickListener) : OtterAdapter<Album, AlbumsGridAdapter.ViewHolder>() { class AlbumsGridAdapter(
private val layoutInflater: LayoutInflater,
private val listener: OnAlbumClickListener
) : FFAAdapter<Album, AlbumsGridAdapter.ViewHolder>() {
private lateinit var binding: RowAlbumGridBinding
interface OnAlbumClickListener { interface OnAlbumClickListener {
fun onClick(view: View?, album: Album) fun onClick(view: View?, album: Album)
} }
@ -24,10 +29,11 @@ class AlbumsGridAdapter(val context: Context?, private val listener: OnAlbumClic
override fun getItemCount() = data.size override fun getItemCount() = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_album_grid, parent, false)
return ViewHolder(view, listener).also { binding = RowAlbumGridBinding.inflate(layoutInflater, parent, false)
view.setOnClickListener(it)
return ViewHolder(binding, listener).also {
binding.root.setOnClickListener(it)
} }
} }
@ -44,9 +50,10 @@ class AlbumsGridAdapter(val context: Context?, private val listener: OnAlbumClic
holder.title.text = album.title holder.title.text = album.title
} }
inner class ViewHolder(view: View, private val listener: OnAlbumClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener { inner class ViewHolder(binding: RowAlbumGridBinding, private val listener: OnAlbumClickListener) :
val cover = view.cover RecyclerView.ViewHolder(binding.root), View.OnClickListener {
val title = view.title val cover = binding.cover
val title = binding.title
override fun onClick(view: View?) { override fun onClick(view: View?) {
listener.onClick(view, data[layoutPosition]) listener.onClick(view, data[layoutPosition])

View File

@ -6,15 +6,22 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.fragments.OtterAdapter import audio.funkwhale.ffa.databinding.RowArtistBinding
import audio.funkwhale.ffa.fragments.FFAAdapter
import audio.funkwhale.ffa.utils.Artist import audio.funkwhale.ffa.utils.Artist
import audio.funkwhale.ffa.utils.maybeLoad import audio.funkwhale.ffa.utils.maybeLoad
import audio.funkwhale.ffa.utils.maybeNormalizeUrl import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_artist.view.*
class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickListener) : OtterAdapter<Artist, ArtistsAdapter.ViewHolder>() { class ArtistsAdapter(
private val layoutInflater: LayoutInflater,
private val context: Context?,
private val listener: OnArtistClickListener
) : FFAAdapter<Artist, ArtistsAdapter.ViewHolder>() {
private lateinit var binding: RowArtistBinding
private var active: List<Artist> = mutableListOf() private var active: List<Artist> = mutableListOf()
interface OnArtistClickListener { interface OnArtistClickListener {
@ -42,10 +49,11 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
override fun getItemId(position: Int) = active[position].id.toLong() override fun getItemId(position: Int) = active[position].id.toLong()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_artist, parent, false)
return ViewHolder(view, listener).also { binding = RowArtistBinding.inflate(layoutInflater, parent, false)
view.setOnClickListener(it)
return ViewHolder(binding, listener).also {
binding.root.setOnClickListener(it)
} }
} }
@ -66,15 +74,22 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
artist.albums?.let { artist.albums?.let {
context?.let { context?.let {
holder.albums.text = context.resources.getQuantityString(R.plurals.album_count, artist.albums.size, artist.albums.size) holder.albums.text = context.resources.getQuantityString(
R.plurals.album_count,
artist.albums.size,
artist.albums.size
)
} }
} }
} }
inner class ViewHolder(view: View, private val listener: OnArtistClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener { inner class ViewHolder(binding: RowArtistBinding, private val listener: OnArtistClickListener) :
val art = view.art RecyclerView.ViewHolder(binding.root),
val name = view.name View.OnClickListener {
val albums = view.albums
val art = binding.art
val name = binding.name
val albums = binding.albums
override fun onClick(view: View?) { override fun onClick(view: View?) {
listener.onClick(view, active[layoutPosition]) listener.onClick(view, active[layoutPosition])

View File

@ -7,17 +7,25 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.RowDownloadBinding
import audio.funkwhale.ffa.playback.PinService import audio.funkwhale.ffa.playback.PinService
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.utils.DownloadInfo
import audio.funkwhale.ffa.utils.Track
import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.Download
import com.google.android.exoplayer2.offline.DownloadService import com.google.android.exoplayer2.offline.DownloadService
import kotlinx.android.synthetic.main.row_download.view.*
class DownloadsAdapter(private val context: Context, private val listener: OnDownloadChangedListener) : RecyclerView.Adapter<DownloadsAdapter.ViewHolder>() { class DownloadsAdapter(
private val layoutInflater: LayoutInflater,
private val context: Context,
private val listener: OnDownloadChangedListener
) : RecyclerView.Adapter<DownloadsAdapter.ViewHolder>() {
interface OnDownloadChangedListener { interface OnDownloadChangedListener {
fun onItemRemoved(index: Int) fun onItemRemoved(index: Int)
} }
private lateinit var binding: RowDownloadBinding
var downloads: MutableList<DownloadInfo> = mutableListOf() var downloads: MutableList<DownloadInfo> = mutableListOf()
override fun getItemCount() = downloads.size override fun getItemCount() = downloads.size
@ -25,9 +33,10 @@ class DownloadsAdapter(private val context: Context, private val listener: OnDow
override fun getItemId(position: Int) = downloads[position].id.toLong() override fun getItemId(position: Int) = downloads[position].id.toLong()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_download, parent, false)
return ViewHolder(view) binding = RowDownloadBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
@ -67,7 +76,12 @@ class DownloadsAdapter(private val context: Context, private val listener: OnDow
holder.toggle.visibility = View.GONE holder.toggle.visibility = View.GONE
} }
Download.STATE_STOPPED -> holder.toggle.setImageIcon(Icon.createWithResource(context, R.drawable.play)) Download.STATE_STOPPED -> holder.toggle.setImageIcon(
Icon.createWithResource(
context,
R.drawable.play
)
)
else -> holder.toggle.setImageIcon(Icon.createWithResource(context, R.drawable.pause)) else -> holder.toggle.setImageIcon(Icon.createWithResource(context, R.drawable.pause))
} }
@ -76,7 +90,13 @@ class DownloadsAdapter(private val context: Context, private val listener: OnDow
holder.toggle.setOnClickListener { holder.toggle.setOnClickListener {
when (state.state) { when (state.state) {
Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> DownloadService.sendSetStopReason(context, PinService::class.java, download.contentId, 1, false) Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> DownloadService.sendSetStopReason(
context,
PinService::class.java,
download.contentId,
1,
false
)
Download.STATE_FAILED -> { Download.STATE_FAILED -> {
Track.fromDownload(download).also { Track.fromDownload(download).also {
@ -84,22 +104,33 @@ class DownloadsAdapter(private val context: Context, private val listener: OnDow
} }
} }
else -> DownloadService.sendSetStopReason(context, PinService::class.java, download.contentId, Download.STOP_REASON_NONE, false) else -> DownloadService.sendSetStopReason(
context,
PinService::class.java,
download.contentId,
Download.STOP_REASON_NONE,
false
)
} }
} }
holder.delete.setOnClickListener { holder.delete.setOnClickListener {
listener.onItemRemoved(position) listener.onItemRemoved(position)
DownloadService.sendRemoveDownload(context, PinService::class.java, download.contentId, false) DownloadService.sendRemoveDownload(
context,
PinService::class.java,
download.contentId,
false
)
} }
} }
} }
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { inner class ViewHolder(binding: RowDownloadBinding) : RecyclerView.ViewHolder(binding.root) {
val title = view.title val title = binding.title
val artist = view.artist val artist = binding.artist
val progress = view.progress val progress = binding.progress
val toggle = view.toggle val toggle = binding.toggle
val delete = view.delete val delete = binding.delete
} }
} }

View File

@ -11,18 +11,31 @@ import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.fragments.OtterAdapter import audio.funkwhale.ffa.databinding.RowTrackBinding
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.fragments.FFAAdapter
import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.Track
import audio.funkwhale.ffa.utils.maybeLoad
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import audio.funkwhale.ffa.utils.toast
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_track.view.* import java.util.Collections
import java.util.*
class FavoritesAdapter(
private val layoutInflater: LayoutInflater,
private val context: Context?,
private val favoriteListener: OnFavoriteListener,
val fromQueue: Boolean = false
) : FFAAdapter<Track, FavoritesAdapter.ViewHolder>() {
class FavoritesAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener, val fromQueue: Boolean = false) : OtterAdapter<Track, FavoritesAdapter.ViewHolder>() {
interface OnFavoriteListener { interface OnFavoriteListener {
fun onToggleFavorite(id: Int, state: Boolean) fun onToggleFavorite(id: Int, state: Boolean)
} }
private lateinit var binding: RowTrackBinding
var currentTrack: Track? = null var currentTrack: Track? = null
override fun getItemCount() = data.size override fun getItemCount() = data.size
@ -32,10 +45,11 @@ class FavoritesAdapter(private val context: Context?, private val favoriteListen
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_track, parent, false)
return ViewHolder(view, context).also { binding = RowTrackBinding.inflate(layoutInflater, parent, false)
view.setOnClickListener(it)
return ViewHolder(binding, context).also {
binding.root.setOnClickListener(it)
} }
} }
@ -76,13 +90,15 @@ class FavoritesAdapter(private val context: Context?, private val favoriteListen
if (favorite.cached && !favorite.downloaded) { if (favorite.cached && !favorite.downloaded) {
holder.title.compoundDrawables.forEach { holder.title.compoundDrawables.forEach {
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN) it?.colorFilter =
PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN)
} }
} }
if (favorite.downloaded) { if (favorite.downloaded) {
holder.title.compoundDrawables.forEach { holder.title.compoundDrawables.forEach {
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN) it?.colorFilter =
PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN)
} }
} }
@ -131,13 +147,15 @@ class FavoritesAdapter(private val context: Context?, private val favoriteListen
CommandBus.send(Command.MoveFromQueue(oldPosition, newPosition)) CommandBus.send(Command.MoveFromQueue(oldPosition, newPosition))
} }
inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view), View.OnClickListener { inner class ViewHolder(binding: RowTrackBinding, val context: Context?) :
val cover = view.cover RecyclerView.ViewHolder(binding.root),
val title = view.title View.OnClickListener {
val artist = view.artist val cover = binding.cover
val title = binding.title
val artist = binding.artist
val favorite = view.favorite val favorite = binding.favorite
val actions = view.actions val actions = binding.actions
override fun onClick(view: View?) { override fun onClick(view: View?) {
when (fromQueue) { when (fromQueue) {

View File

@ -4,24 +4,42 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.view.* import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.fragments.OtterAdapter import audio.funkwhale.ffa.databinding.RowTrackBinding
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.fragments.FFAAdapter
import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.PlaylistTrack
import audio.funkwhale.ffa.utils.Track
import audio.funkwhale.ffa.utils.maybeLoad
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import audio.funkwhale.ffa.utils.toast
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_track.view.* import java.util.Collections
import java.util.*
class PlaylistTracksAdapter(
private val layoutInflater: LayoutInflater,
private val context: Context?,
private val favoriteListener: OnFavoriteListener? = null,
private val playlistListener: OnPlaylistListener? = null
) : FFAAdapter<PlaylistTrack, PlaylistTracksAdapter.ViewHolder>() {
class PlaylistTracksAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener? = null, private val playlistListener: OnPlaylistListener? = null) : OtterAdapter<PlaylistTrack, PlaylistTracksAdapter.ViewHolder>() {
interface OnFavoriteListener { interface OnFavoriteListener {
fun onToggleFavorite(id: Int, state: Boolean) fun onToggleFavorite(id: Int, state: Boolean)
} }
private lateinit var binding: RowTrackBinding
interface OnPlaylistListener { interface OnPlaylistListener {
fun onMoveTrack(from: Int, to: Int) fun onMoveTrack(from: Int, to: Int)
fun onRemoveTrackFromPlaylist(track: Track, index: Int) fun onRemoveTrackFromPlaylist(track: Track, index: Int)
@ -46,10 +64,11 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_track, parent, false)
return ViewHolder(view, context).also { binding = RowTrackBinding.inflate(layoutInflater, parent, false)
view.setOnClickListener(it)
return ViewHolder(binding, context).also {
binding.root.setOnClickListener(it)
} }
} }
@ -105,7 +124,10 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track.track))) R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track.track)))
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track.track)) R.id.track_play_next -> CommandBus.send(Command.PlayNext(track.track))
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track.track)) R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track.track))
R.id.track_remove_from_playlist -> playlistListener?.onRemoveTrackFromPlaylist(track.track, position) R.id.track_remove_from_playlist -> playlistListener?.onRemoveTrackFromPlaylist(
track.track,
position
)
} }
true true
@ -141,14 +163,16 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
notifyItemMoved(oldPosition, newPosition) notifyItemMoved(oldPosition, newPosition)
} }
inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view), View.OnClickListener { inner class ViewHolder(binding: RowTrackBinding, val context: Context?) :
val handle = view.handle RecyclerView.ViewHolder(binding.root),
val cover = view.cover View.OnClickListener {
val title = view.title val handle = binding.handle
val artist = view.artist val cover = binding.cover
val title = binding.title
val artist = binding.artist
val favorite = view.favorite val favorite = binding.favorite
val actions = view.actions val actions = binding.actions
override fun onClick(view: View?) { override fun onClick(view: View?) {
data.subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition)).apply { data.subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition)).apply {
@ -170,7 +194,11 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) = override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) =
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
if (from == -1) from = viewHolder.adapterPosition if (from == -1) from = viewHolder.adapterPosition
to = target.adapterPosition to = target.adapterPosition

View File

@ -7,27 +7,34 @@ import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.fragments.OtterAdapter import audio.funkwhale.ffa.databinding.RowPlaylistBinding
import audio.funkwhale.ffa.fragments.FFAAdapter
import audio.funkwhale.ffa.utils.Playlist import audio.funkwhale.ffa.utils.Playlist
import audio.funkwhale.ffa.utils.toDurationString import audio.funkwhale.ffa.utils.toDurationString
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_playlist.view.*
class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistClickListener) : OtterAdapter<Playlist, PlaylistsAdapter.ViewHolder>() { class PlaylistsAdapter(
private val layoutInflater: LayoutInflater,
private val context: Context?,
private val listener: OnPlaylistClickListener
) : FFAAdapter<Playlist, PlaylistsAdapter.ViewHolder>() {
interface OnPlaylistClickListener { interface OnPlaylistClickListener {
fun onClick(holder: View?, playlist: Playlist) fun onClick(holder: View?, playlist: Playlist)
} }
private lateinit var binding: RowPlaylistBinding
override fun getItemCount() = data.size override fun getItemCount() = data.size
override fun getItemId(position: Int) = data[position].id.toLong() override fun getItemId(position: Int) = data[position].id.toLong()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_playlist, parent, false) binding = RowPlaylistBinding.inflate(layoutInflater, parent, false)
return ViewHolder(view, listener).also { return ViewHolder(binding, listener).also {
view.setOnClickListener(it) binding.root.setOnClickListener(it)
} }
} }
@ -35,24 +42,29 @@ class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistCl
val playlist = data[position] val playlist = data[position]
holder.name.text = playlist.name holder.name.text = playlist.name
holder.summary.text = context?.resources?.getQuantityString(R.plurals.playlist_description, playlist.tracks_count, playlist.tracks_count, toDurationString(playlist.duration.toLong())) ?: "" holder.summary.text = context?.resources?.getQuantityString(
R.plurals.playlist_description,
playlist.tracks_count,
playlist.tracks_count,
toDurationString(playlist.duration.toLong())
) ?: ""
context?.let { context?.let {
ContextCompat.getDrawable(context, R.drawable.cover).let { ContextCompat.getDrawable(context, R.drawable.cover).let {
holder.cover_top_left.setImageDrawable(it) holder.coverTopLeft.setImageDrawable(it)
holder.cover_top_right.setImageDrawable(it) holder.covertTopRight.setImageDrawable(it)
holder.cover_bottom_left.setImageDrawable(it) holder.coverBottomLeft.setImageDrawable(it)
holder.cover_bottom_right.setImageDrawable(it) holder.coverBottomRight.setImageDrawable(it)
} }
} }
playlist.album_covers.shuffled().take(4).forEachIndexed { index, url -> playlist.album_covers.shuffled().take(4).forEachIndexed { index, url ->
val imageView = when (index) { val imageView = when (index) {
0 -> holder.cover_top_left 0 -> holder.coverTopLeft
1 -> holder.cover_top_right 1 -> holder.covertTopRight
2 -> holder.cover_bottom_left 2 -> holder.coverBottomLeft
3 -> holder.cover_bottom_right 3 -> holder.coverBottomRight
else -> holder.cover_top_left else -> holder.coverTopLeft
} }
val corner = when (index) { val corner = when (index) {
@ -70,14 +82,18 @@ class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistCl
} }
} }
inner class ViewHolder(view: View, private val listener: OnPlaylistClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener { inner class ViewHolder(
val name = view.name binding: RowPlaylistBinding,
val summary = view.summary private val listener: OnPlaylistClickListener
) :
RecyclerView.ViewHolder(binding.root), View.OnClickListener {
val name = binding.name
val summary = binding.summary
val cover_top_left = view.cover_top_left val coverTopLeft = binding.coverTopLeft
val cover_top_right = view.cover_top_right val covertTopRight = binding.coverTopRight
val cover_bottom_left = view.cover_bottom_left val coverBottomLeft = binding.coverBottomLeft
val cover_bottom_right = view.cover_bottom_right val coverBottomRight = binding.coverBottomRight
override fun onClick(view: View?) { override fun onClick(view: View?) {
listener.onClick(view, data[layoutPosition]) listener.onClick(view, data[layoutPosition])

View File

@ -6,25 +6,34 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.fragments.OtterAdapter import audio.funkwhale.ffa.databinding.RowRadioBinding
import audio.funkwhale.ffa.databinding.RowRadioHeaderBinding
import audio.funkwhale.ffa.fragments.FFAAdapter
import audio.funkwhale.ffa.utils.AppContext import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.Event import audio.funkwhale.ffa.utils.Event
import audio.funkwhale.ffa.utils.EventBus import audio.funkwhale.ffa.utils.EventBus
import audio.funkwhale.ffa.utils.Radio import audio.funkwhale.ffa.utils.Radio
import audio.funkwhale.ffa.views.LoadingImageView import audio.funkwhale.ffa.views.LoadingImageView
import com.preference.PowerPreference import com.preference.PowerPreference
import kotlinx.android.synthetic.main.row_radio.view.*
import kotlinx.android.synthetic.main.row_radio_header.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private val listener: OnRadioClickListener) : OtterAdapter<Radio, RadiosAdapter.ViewHolder>() { class RadiosAdapter(
private val layoutInflater: LayoutInflater,
private val context: Context?,
private val scope: CoroutineScope,
private val listener: OnRadioClickListener
) : FFAAdapter<Radio, RadiosAdapter.ViewHolder>() {
interface OnRadioClickListener { interface OnRadioClickListener {
fun onClick(holder: ViewHolder, radio: Radio) fun onClick(holder: RowRadioViewHolder, radio: Radio)
} }
private lateinit var rowRadioBinding: RowRadioBinding
private lateinit var rowRadioHeaderBinding: RowRadioHeaderBinding
enum class RowType { enum class RowType {
Header, Header,
InstanceRadio, InstanceRadio,
@ -33,16 +42,43 @@ class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private va
private val instanceRadios: List<Radio> by lazy { private val instanceRadios: List<Radio> by lazy {
context?.let { context?.let {
return@lazy when (val username = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("actor_username")) { return@lazy when (val username =
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("actor_username")) {
"" -> listOf( "" -> listOf(
Radio(0, "random", context.getString(R.string.radio_random_title), context.getString(R.string.radio_random_description)) Radio(
0,
"random",
context.getString(R.string.radio_random_title),
context.getString(R.string.radio_random_description)
)
) )
else -> listOf( else -> listOf(
Radio(0, "actor_content", context.getString(R.string.radio_your_content_title), context.getString(R.string.radio_your_content_description), username), Radio(
Radio(0, "random", context.getString(R.string.radio_random_title), context.getString(R.string.radio_random_description)), 0,
Radio(0, "favorites", context.getString(R.string.favorites), context.getString(R.string.radio_favorites_description)), "actor_content",
Radio(0, "less-listened", context.getString(R.string.radio_less_listened_title), context.getString(R.string.radio_less_listened_description)) context.getString(R.string.radio_your_content_title),
context.getString(R.string.radio_your_content_description),
username
),
Radio(
0,
"random",
context.getString(R.string.radio_random_title),
context.getString(R.string.radio_random_description)
),
Radio(
0,
"favorites",
context.getString(R.string.favorites),
context.getString(R.string.radio_favorites_description)
),
Radio(
0,
"less-listened",
context.getString(R.string.radio_less_listened_title),
context.getString(R.string.radio_less_listened_description)
)
) )
} }
} }
@ -76,31 +112,36 @@ class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private va
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RadiosAdapter.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RadiosAdapter.ViewHolder {
return when (viewType) { return when (viewType) {
RowType.InstanceRadio.ordinal, RowType.UserRadio.ordinal -> { RowType.InstanceRadio.ordinal, RowType.UserRadio.ordinal -> {
val view = LayoutInflater.from(context).inflate(R.layout.row_radio, parent, false) rowRadioBinding = RowRadioBinding.inflate(layoutInflater, parent, false)
ViewHolder(view, listener).also { RowRadioViewHolder(rowRadioBinding, listener).also {
view.setOnClickListener(it) rowRadioBinding.root.setOnClickListener(it)
} }
} }
else -> ViewHolder(LayoutInflater.from(context).inflate(R.layout.row_radio_header, parent, false), null) else -> {
rowRadioHeaderBinding = RowRadioHeaderBinding.inflate(layoutInflater, parent, false)
RowRadioHeaderViewHolder(rowRadioHeaderBinding)
}
} }
} }
override fun onBindViewHolder(holder: RadiosAdapter.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RadiosAdapter.ViewHolder, position: Int) {
when (getItemViewType(position)) { when (getItemViewType(position)) {
RowType.Header.ordinal -> { RowType.Header.ordinal -> {
holder as RowRadioHeaderViewHolder
context?.let { context?.let {
when (position) { when (position) {
0 -> holder.label.text = context.getString(R.string.radio_instance_radios) 0 -> holder.label.text = context.getString(R.string.radio_instance_radios)
instanceRadios.size + 1 -> holder.label.text = context.getString(R.string.radio_user_radios) instanceRadios.size + 1 -> holder.label.text =
context.getString(R.string.radio_user_radios)
} }
} }
} }
RowType.InstanceRadio.ordinal, RowType.UserRadio.ordinal -> { RowType.InstanceRadio.ordinal, RowType.UserRadio.ordinal -> {
val radio = getRadioAt(position) val radio = getRadioAt(position)
holder as RowRadioViewHolder
holder.art.visibility = View.VISIBLE holder.art.visibility = View.VISIBLE
holder.name.text = radio.name holder.name.text = radio.name
holder.description.text = radio.description holder.description.text = radio.description
@ -126,17 +167,12 @@ class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private va
} }
} }
inner class ViewHolder(view: View, private val listener: OnRadioClickListener?) : RecyclerView.ViewHolder(view), View.OnClickListener { inner class RowRadioViewHolder(binding: RowRadioBinding, val listener: OnRadioClickListener) :
val label = view.label ViewHolder(binding.root),
val art = view.art View.OnClickListener {
val name = view.name val art = binding.art
val description = view.description val name = binding.name
val description = binding.description
var native = false
override fun onClick(view: View?) {
listener?.onClick(this, getRadioAt(layoutPosition))
}
fun spin() { fun spin() {
context?.let { context?.let {
@ -151,7 +187,6 @@ class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private va
when (message) { when (message) {
is Event.RadioStarted -> { is Event.RadioStarted -> {
art.colorFilter = originalColorFilter art.colorFilter = originalColorFilter
LoadingImageView.stop(context, originalDrawable, art, imageAnimator) LoadingImageView.stop(context, originalDrawable, art, imageAnimator)
} }
} }
@ -159,5 +194,21 @@ class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private va
} }
} }
} }
override fun onClick(view: View?) {
listener.onClick(this, getRadioAt(layoutPosition))
}
}
inner class RowRadioHeaderViewHolder(
binding: RowRadioHeaderBinding
) : ViewHolder(
binding.root
) {
val label = binding.label
}
abstract inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
var native = false
} }
} }

View File

@ -13,12 +13,27 @@ import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.databinding.RowSearchHeaderBinding
import audio.funkwhale.ffa.databinding.RowTrackBinding
import audio.funkwhale.ffa.utils.Album
import audio.funkwhale.ffa.utils.Artist
import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.Track
import audio.funkwhale.ffa.utils.maybeLoad
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import audio.funkwhale.ffa.utils.onApi
import audio.funkwhale.ffa.utils.toast
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_track.view.*
class SearchAdapter(private val context: Context?, private val listener: OnSearchResultClickListener? = null, private val favoriteListener: OnFavoriteListener? = null) : RecyclerView.Adapter<SearchAdapter.ViewHolder>() { class SearchAdapter(
private val layoutInflater: LayoutInflater,
private val context: Context?,
private val listener: OnSearchResultClickListener? = null,
private val favoriteListener: OnFavoriteListener? = null
) : RecyclerView.Adapter<SearchAdapter.ViewHolder>() {
interface OnSearchResultClickListener { interface OnSearchResultClickListener {
fun onArtistClick(holder: View?, artist: Artist) fun onArtistClick(holder: View?, artist: Artist)
fun onAlbumClick(holder: View?, album: Album) fun onAlbumClick(holder: View?, album: Album)
@ -35,7 +50,10 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
Track Track
} }
val SECTION_COUNT = 3 private lateinit var searchHeaderBinding: RowSearchHeaderBinding
private lateinit var rowTrackBinding: RowTrackBinding
val sectionCount = 3
var artists: MutableList<Artist> = mutableListOf() var artists: MutableList<Artist> = mutableListOf()
var albums: MutableList<Album> = mutableListOf() var albums: MutableList<Album> = mutableListOf()
@ -43,7 +61,7 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
var currentTrack: Track? = null var currentTrack: Track? = null
override fun getItemCount() = SECTION_COUNT + artists.size + albums.size + tracks.size override fun getItemCount() = sectionCount + artists.size + albums.size + tracks.size
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
return when (getItemViewType(position)) { return when (getItemViewType(position)) {
@ -55,7 +73,7 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
ResultType.Artist.ordinal -> artists[position].id.toLong() ResultType.Artist.ordinal -> artists[position].id.toLong()
ResultType.Artist.ordinal -> albums[position - artists.size - 2].id.toLong() ResultType.Artist.ordinal -> albums[position - artists.size - 2].id.toLong()
ResultType.Track.ordinal -> tracks[position - artists.size - albums.size - SECTION_COUNT].id.toLong() ResultType.Track.ordinal -> tracks[position - artists.size - albums.size - sectionCount].id.toLong()
else -> 0 else -> 0
} }
} }
@ -72,26 +90,35 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = when (viewType) { return when (viewType) {
ResultType.Header.ordinal -> LayoutInflater.from(context).inflate(R.layout.row_search_header, parent, false) ResultType.Header.ordinal -> {
else -> LayoutInflater.from(context).inflate(R.layout.row_track, parent, false) searchHeaderBinding = RowSearchHeaderBinding.inflate(layoutInflater, parent, false)
} SearchHeaderViewHolder(searchHeaderBinding, context)
}
return ViewHolder(view, context).also { else -> {
view.setOnClickListener(it) rowTrackBinding = RowTrackBinding.inflate(layoutInflater, parent, false)
RowTrackViewHolder(rowTrackBinding, context).also {
rowTrackBinding.root.setOnClickListener(it)
}
}
} }
} }
@SuppressLint("NewApi") @SuppressLint("NewApi")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val resultType = getItemViewType(position) val resultType = getItemViewType(position)
val searchHeaderViewHolder = holder as? SearchHeaderViewHolder
val rowTrackViewHolder = holder as? RowTrackViewHolder
if (resultType == ResultType.Header.ordinal) { if (resultType == ResultType.Header.ordinal) {
context?.let { context -> context?.let { context ->
if (position == 0) { if (position == 0) {
holder.title.text = context.getString(R.string.artists) searchHeaderViewHolder?.title?.text = context.getString(R.string.artists)
holder.itemView.visibility = View.VISIBLE holder.itemView.visibility = View.VISIBLE
holder.itemView.layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) holder.itemView.layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
if (artists.isEmpty()) { if (artists.isEmpty()) {
holder.itemView.visibility = View.GONE holder.itemView.visibility = View.GONE
@ -100,9 +127,12 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
} }
if (position == (artists.size + 1)) { if (position == (artists.size + 1)) {
holder.title.text = context.getString(R.string.albums) searchHeaderViewHolder?.title?.text = context.getString(R.string.albums)
holder.itemView.visibility = View.VISIBLE holder.itemView.visibility = View.VISIBLE
holder.itemView.layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) holder.itemView.layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
if (albums.isEmpty()) { if (albums.isEmpty()) {
holder.itemView.visibility = View.GONE holder.itemView.visibility = View.GONE
@ -111,9 +141,12 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
} }
if (position == (artists.size + albums.size + 2)) { if (position == (artists.size + albums.size + 2)) {
holder.title.text = context.getString(R.string.tracks) searchHeaderViewHolder?.title?.text = context.getString(R.string.tracks)
holder.itemView.visibility = View.VISIBLE holder.itemView.visibility = View.VISIBLE
holder.itemView.layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) holder.itemView.layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
if (tracks.isEmpty()) { if (tracks.isEmpty()) {
holder.itemView.visibility = View.GONE holder.itemView.visibility = View.GONE
@ -127,20 +160,20 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
val item = when (resultType) { val item = when (resultType) {
ResultType.Artist.ordinal -> { ResultType.Artist.ordinal -> {
holder.actions.visibility = View.GONE rowTrackViewHolder?.actions?.visibility = View.GONE
holder.favorite.visibility = View.GONE rowTrackViewHolder?.favorite?.visibility = View.GONE
artists[position - 1] artists[position - 1]
} }
ResultType.Album.ordinal -> { ResultType.Album.ordinal -> {
holder.actions.visibility = View.GONE rowTrackViewHolder?.actions?.visibility = View.GONE
holder.favorite.visibility = View.GONE rowTrackViewHolder?.favorite?.visibility = View.GONE
albums[position - artists.size - 2] albums[position - artists.size - 2]
} }
ResultType.Track.ordinal -> tracks[position - artists.size - albums.size - SECTION_COUNT] ResultType.Track.ordinal -> tracks[position - artists.size - albums.size - sectionCount]
else -> tracks[position] else -> tracks[position]
} }
@ -149,65 +182,98 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
.maybeLoad(maybeNormalizeUrl(item.cover())) .maybeLoad(maybeNormalizeUrl(item.cover()))
.fit() .fit()
.transform(RoundedCornersTransformation(16, 0)) .transform(RoundedCornersTransformation(16, 0))
.into(holder.cover) .into(rowTrackViewHolder?.cover)
holder.title.text = item.title() searchHeaderViewHolder?.title?.text = item.title()
holder.artist.text = item.subtitle() rowTrackViewHolder?.artist?.text = item.subtitle()
Build.VERSION_CODES.P.onApi( Build.VERSION_CODES.P.onApi(
{ {
holder.title.setTypeface(holder.title.typeface, Typeface.DEFAULT.weight) searchHeaderViewHolder?.title?.setTypeface(
holder.artist.setTypeface(holder.artist.typeface, Typeface.DEFAULT.weight) searchHeaderViewHolder.title.typeface,
Typeface.DEFAULT.weight
)
rowTrackViewHolder?.artist?.setTypeface(
rowTrackViewHolder.artist.typeface,
Typeface.DEFAULT.weight
)
}, },
{ {
holder.title.typeface = Typeface.create(holder.title.typeface, Typeface.NORMAL) searchHeaderViewHolder?.title?.typeface =
holder.artist.typeface = Typeface.create(holder.artist.typeface, Typeface.NORMAL) Typeface.create(searchHeaderViewHolder?.title?.typeface, Typeface.NORMAL)
rowTrackViewHolder?.artist?.typeface =
Typeface.create(rowTrackViewHolder?.artist?.typeface, Typeface.NORMAL)
}) })
holder.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0) searchHeaderViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
if (resultType == ResultType.Track.ordinal) { if (resultType == ResultType.Track.ordinal) {
(item as? Track)?.let { track -> (item as? Track)?.let { track ->
context?.let { context -> context?.let { context ->
if (track == currentTrack || track.current) { if (track == currentTrack || track.current) {
holder.title.setTypeface(holder.title.typeface, Typeface.BOLD) searchHeaderViewHolder?.title?.setTypeface(
holder.artist.setTypeface(holder.artist.typeface, Typeface.BOLD) searchHeaderViewHolder.title.typeface,
Typeface.BOLD
)
rowTrackViewHolder?.artist?.setTypeface(
rowTrackViewHolder.artist.typeface,
Typeface.BOLD
)
} }
when (track.favorite) { when (track.favorite) {
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite)) true -> rowTrackViewHolder?.favorite?.setColorFilter(context.getColor(R.color.colorFavorite))
false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected)) false -> rowTrackViewHolder?.favorite?.setColorFilter(context.getColor(R.color.colorSelected))
} }
holder.favorite.setOnClickListener { rowTrackViewHolder?.favorite?.setOnClickListener {
favoriteListener?.let { favoriteListener?.let {
favoriteListener.onToggleFavorite(track.id, !track.favorite) favoriteListener.onToggleFavorite(track.id, !track.favorite)
tracks[position - artists.size - albums.size - SECTION_COUNT].favorite = !track.favorite tracks[position - artists.size - albums.size - sectionCount].favorite =
!track.favorite
notifyItemChanged(position) notifyItemChanged(position)
} }
} }
when (track.cached || track.downloaded) { when (track.cached || track.downloaded) {
true -> holder.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0) true -> searchHeaderViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(
false -> holder.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0) R.drawable.downloaded,
0,
0,
0
)
false -> searchHeaderViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(
0,
0,
0,
0
)
} }
if (track.cached && !track.downloaded) { if (track.cached && !track.downloaded) {
holder.title.compoundDrawables.forEach { searchHeaderViewHolder?.title?.compoundDrawables?.forEach {
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN) it?.colorFilter =
PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN)
} }
} }
if (track.downloaded) { if (track.downloaded) {
holder.title.compoundDrawables.forEach { searchHeaderViewHolder?.title?.compoundDrawables?.forEach {
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN) it?.colorFilter =
PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN)
} }
} }
holder.actions.setOnClickListener { rowTrackViewHolder?.actions?.setOnClickListener {
PopupMenu(context, holder.actions, Gravity.START, R.attr.actionOverflowMenuStyle, 0).apply { PopupMenu(
context,
rowTrackViewHolder.actions,
Gravity.START,
R.attr.actionOverflowMenuStyle,
0
).apply {
inflate(R.menu.row_track) inflate(R.menu.row_track)
setOnMenuItemClickListener { setOnMenuItemClickListener {
@ -234,19 +300,23 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
return when (type) { return when (type) {
ResultType.Artist -> position + 1 ResultType.Artist -> position + 1
ResultType.Album -> position + artists.size + 2 ResultType.Album -> position + artists.size + 2
ResultType.Track -> artists.size + albums.size + SECTION_COUNT + position ResultType.Track -> artists.size + albums.size + sectionCount + position
else -> 0 else -> 0
} }
} }
inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view), View.OnClickListener { inner class SearchHeaderViewHolder(val binding: RowSearchHeaderBinding, context: Context?) :
val handle = view.handle ViewHolder(binding.root, context) {
val cover = view.cover val title = binding.title
val title = view.title }
val artist = view.artist
val favorite = view.favorite inner class RowTrackViewHolder(val binding: RowTrackBinding, context: Context?) :
val actions = view.actions ViewHolder(binding.root, context), View.OnClickListener {
val cover = binding.cover
val artist = binding.artist
val favorite = binding.favorite
val actions = binding.actions
override fun onClick(view: View?) { override fun onClick(view: View?) {
when (getItemViewType(layoutPosition)) { when (getItemViewType(layoutPosition)) {
@ -263,7 +333,7 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
} }
ResultType.Track.ordinal -> { ResultType.Track.ordinal -> {
val position = layoutPosition - artists.size - albums.size - SECTION_COUNT val position = layoutPosition - artists.size - albums.size - sectionCount
tracks.subList(position, tracks.size).plus(tracks.subList(0, position)).apply { tracks.subList(position, tracks.size).plus(tracks.subList(0, position)).apply {
CommandBus.send(Command.ReplaceQueue(this)) CommandBus.send(Command.ReplaceQueue(this))
@ -273,8 +343,11 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
} }
else -> { else -> {
// empty
} }
} }
} }
} }
abstract inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view)
} }

View File

@ -2,26 +2,44 @@ package audio.funkwhale.ffa.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.* import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.view.* import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.fragments.OtterAdapter import audio.funkwhale.ffa.databinding.RowTrackBinding
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.fragments.FFAAdapter
import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.Track
import audio.funkwhale.ffa.utils.maybeLoad
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import audio.funkwhale.ffa.utils.toast
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_track.view.* import java.util.Collections
import java.util.*
class TracksAdapter(
private val layoutInflater: LayoutInflater,
private val context: Context?,
private val favoriteListener: OnFavoriteListener? = null,
val fromQueue: Boolean = false
) : FFAAdapter<Track, TracksAdapter.ViewHolder>() {
class TracksAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener? = null, val fromQueue: Boolean = false) : OtterAdapter<Track, TracksAdapter.ViewHolder>() {
interface OnFavoriteListener { interface OnFavoriteListener {
fun onToggleFavorite(id: Int, state: Boolean) fun onToggleFavorite(id: Int, state: Boolean)
} }
private lateinit var binding: RowTrackBinding
private lateinit var touchHelper: ItemTouchHelper private lateinit var touchHelper: ItemTouchHelper
var currentTrack: Track? = null var currentTrack: Track? = null
@ -41,10 +59,11 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_track, parent, false)
return ViewHolder(view, context).also { binding = RowTrackBinding.inflate(layoutInflater, parent, false)
view.setOnClickListener(it)
return ViewHolder(binding, context).also {
binding.root.setOnClickListener(it)
} }
} }
@ -94,13 +113,15 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
if (track.cached && !track.downloaded) { if (track.cached && !track.downloaded) {
holder.title.compoundDrawables.forEach { holder.title.compoundDrawables.forEach {
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN) it?.colorFilter =
PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN)
} }
} }
if (track.downloaded) { if (track.downloaded) {
holder.title.compoundDrawables.forEach { holder.title.compoundDrawables.forEach {
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN) it?.colorFilter =
PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN)
} }
} }
} }
@ -154,14 +175,17 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
notifyItemMoved(oldPosition, newPosition) notifyItemMoved(oldPosition, newPosition)
} }
inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view), View.OnClickListener { inner class ViewHolder(binding: RowTrackBinding, val context: Context?) :
val handle = view.handle RecyclerView.ViewHolder(binding.root),
val cover = view.cover View.OnClickListener {
val title = view.title
val artist = view.artist
val favorite = view.favorite val handle = binding.handle
val actions = view.actions val cover = binding.cover
val title = binding.title
val artist = binding.artist
val favorite = binding.favorite
val actions = binding.actions
override fun onClick(view: View?) { override fun onClick(view: View?) {
when (fromQueue) { when (fromQueue) {
@ -188,10 +212,14 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) = override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) =
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { override fun onMove(
to = target.adapterPosition recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
to = target.absoluteAdapterPosition
onItemMove(viewHolder.adapterPosition, to) onItemMove(viewHolder.absoluteAdapterPosition, to)
return true return true
} }

View File

@ -2,16 +2,18 @@ package audio.funkwhale.ffa.fragments
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.PlaylistsAdapter import audio.funkwhale.ffa.adapters.PlaylistsAdapter
import audio.funkwhale.ffa.databinding.DialogAddToPlaylistBinding
import audio.funkwhale.ffa.repositories.ManagementPlaylistsRepository import audio.funkwhale.ffa.repositories.ManagementPlaylistsRepository
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.utils.*
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.android.synthetic.main.dialog_add_to_playlist.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -19,10 +21,18 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
object AddToPlaylistDialog { object AddToPlaylistDialog {
fun show(activity: Activity, lifecycleScope: CoroutineScope, tracks: List<Track>) {
fun show(
layoutInflater: LayoutInflater,
activity: Activity,
lifecycleScope: CoroutineScope,
tracks: List<Track>
) {
val binding = DialogAddToPlaylistBinding.inflate(layoutInflater)
val dialog = AlertDialog.Builder(activity).run { val dialog = AlertDialog.Builder(activity).run {
setTitle(activity.getString(R.string.playlist_add_to)) setTitle(activity.getString(R.string.playlist_add_to))
setView(activity.layoutInflater.inflate(R.layout.dialog_add_to_playlist, null)) setView(binding.root)
create() create()
} }
@ -31,12 +41,22 @@ object AddToPlaylistDialog {
val repository = ManagementPlaylistsRepository(activity) val repository = ManagementPlaylistsRepository(activity)
dialog.name.editText?.addTextChangedListener { binding.name.editText?.addTextChangedListener(object : TextWatcher {
dialog.create.isEnabled = !(dialog.name.editText?.text?.trim()?.isBlank() ?: true) override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
} // empty
}
dialog.create.setOnClickListener { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val name = dialog.name.editText?.text?.toString()?.trim() ?: "" // empty
}
override fun afterTextChanged(s: Editable?) {
binding.create.isEnabled = !(binding.name.editText?.text?.trim()?.isBlank() ?: true)
}
})
binding.create.setOnClickListener {
val name = binding.name.editText?.text?.toString()?.trim() ?: ""
if (name.isEmpty()) return@setOnClickListener if (name.isEmpty()) return@setOnClickListener
@ -45,7 +65,11 @@ object AddToPlaylistDialog {
repository.add(id, tracks) repository.add(id, tracks)
withContext(Main) { withContext(Main) {
Toast.makeText(activity, activity.getString(R.string.playlist_added_to, name), Toast.LENGTH_SHORT).show() Toast.makeText(
activity,
activity.getString(R.string.playlist_added_to, name),
Toast.LENGTH_SHORT
).show()
} }
dialog.dismiss() dialog.dismiss()
@ -53,18 +77,23 @@ object AddToPlaylistDialog {
} }
} }
val adapter = PlaylistsAdapter(activity, object : PlaylistsAdapter.OnPlaylistClickListener { val adapter =
override fun onClick(holder: View?, playlist: Playlist) { PlaylistsAdapter(layoutInflater, activity, object : PlaylistsAdapter.OnPlaylistClickListener {
repository.add(playlist.id, tracks) override fun onClick(holder: View?, playlist: Playlist) {
repository.add(playlist.id, tracks)
Toast.makeText(activity, activity.getString(R.string.playlist_added_to, playlist.name), Toast.LENGTH_SHORT).show() Toast.makeText(
activity,
activity.getString(R.string.playlist_added_to, playlist.name),
Toast.LENGTH_SHORT
).show()
dialog.dismiss() dialog.dismiss()
} }
}) })
dialog.playlists.layoutManager = LinearLayoutManager(activity) binding.playlists.layoutManager = LinearLayoutManager(activity)
dialog.playlists.adapter = adapter binding.playlists.adapter = adapter
repository.apply { repository.apply {
var first = true var first = true

View File

@ -2,9 +2,12 @@ package audio.funkwhale.ffa.fragments
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -16,13 +19,13 @@ import androidx.transition.Slide
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.activities.MainActivity import audio.funkwhale.ffa.activities.MainActivity
import audio.funkwhale.ffa.adapters.AlbumsAdapter import audio.funkwhale.ffa.adapters.AlbumsAdapter
import audio.funkwhale.ffa.databinding.FragmentAlbumsBinding
import audio.funkwhale.ffa.repositories.AlbumsRepository import audio.funkwhale.ffa.repositories.AlbumsRepository
import audio.funkwhale.ffa.repositories.ArtistTracksRepository import audio.funkwhale.ffa.repositories.ArtistTracksRepository
import audio.funkwhale.ffa.repositories.Repository import audio.funkwhale.ffa.repositories.Repository
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.utils.*
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.fragment_albums.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -30,16 +33,19 @@ import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class AlbumsFragment : OtterFragment<Album, AlbumsAdapter>() { class AlbumsFragment : FFAFragment<Album, AlbumsAdapter>() {
override val viewRes = R.layout.fragment_albums
override val recycler: RecyclerView get() = albums override val recycler: RecyclerView get() = binding.albums
override val alwaysRefresh = false override val alwaysRefresh = false
private var _binding: FragmentAlbumsBinding? = null
private val binding get() = _binding!!
private lateinit var artistTracksRepository: ArtistTracksRepository private lateinit var artistTracksRepository: ArtistTracksRepository
var artistId = 0 private var artistId = 0
var artistName = "" private var artistName = ""
var artistArt = "" private var artistArt = ""
companion object { companion object {
fun new(artist: Artist, _art: String? = null): AlbumsFragment { fun new(artist: Artist, _art: String? = null): AlbumsFragment {
@ -100,15 +106,30 @@ class AlbumsFragment : OtterFragment<Album, AlbumsAdapter>() {
artistArt = getString("artistArt") ?: "" artistArt = getString("artistArt") ?: ""
} }
adapter = AlbumsAdapter(context, OnAlbumClickListener()) adapter = AlbumsAdapter(layoutInflater, context, OnAlbumClickListener())
repository = AlbumsRepository(context, artistId) repository = AlbumsRepository(context, artistId)
artistTracksRepository = ArtistTracksRepository(context, artistId) artistTracksRepository = ArtistTracksRepository(context, artistId)
} }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAlbumsBinding.inflate(inflater)
swiper = binding.swiper
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
cover?.let { cover -> binding.cover.let { cover ->
Picasso.get() Picasso.get()
.maybeLoad(maybeNormalizeUrl(artistArt)) .maybeLoad(maybeNormalizeUrl(artistArt))
.noFade() .noFade()
@ -118,9 +139,9 @@ class AlbumsFragment : OtterFragment<Album, AlbumsAdapter>() {
.into(cover) .into(cover)
} }
artist.text = artistName binding.artist.text = artistName
play.setOnClickListener { binding.play.setOnClickListener {
val loader = CircularProgressDrawable(requireContext()).apply { val loader = CircularProgressDrawable(requireContext()).apply {
setColorSchemeColors(ContextCompat.getColor(requireContext(), android.R.color.white)) setColorSchemeColors(ContextCompat.getColor(requireContext(), android.R.color.white))
strokeWidth = 4f strokeWidth = 4f
@ -128,8 +149,8 @@ class AlbumsFragment : OtterFragment<Album, AlbumsAdapter>() {
loader.start() loader.start()
play.icon = loader binding.play.icon = loader
play.isClickable = false binding.play.isClickable = false
lifecycleScope.launch(IO) { lifecycleScope.launch(IO) {
artistTracksRepository.fetch(Repository.Origin.Network.origin) artistTracksRepository.fetch(Repository.Origin.Network.origin)
@ -141,8 +162,9 @@ class AlbumsFragment : OtterFragment<Album, AlbumsAdapter>() {
CommandBus.send(Command.ReplaceQueue(it)) CommandBus.send(Command.ReplaceQueue(it))
withContext(Main) { withContext(Main) {
play.icon = requireContext().getDrawable(R.drawable.play) binding.play.icon =
play.isClickable = true AppCompatResources.getDrawable(binding.root.context, R.drawable.play)
binding.play.isClickable = true
} }
} }
} }
@ -154,15 +176,15 @@ class AlbumsFragment : OtterFragment<Album, AlbumsAdapter>() {
var coverHeight: Float? = null var coverHeight: Float? = null
scroller.setOnScrollChangeListener { _: View?, _: Int, scrollY: Int, _: Int, _: Int -> binding.scroller.setOnScrollChangeListener { _: View?, _: Int, scrollY: Int, _: Int, _: Int ->
if (coverHeight == null) { if (coverHeight == null) {
coverHeight = cover.measuredHeight.toFloat() coverHeight = binding.cover.measuredHeight.toFloat()
} }
cover.translationY = (scrollY / 2).toFloat() binding.cover.translationY = (scrollY / 2).toFloat()
coverHeight?.let { height -> coverHeight?.let { height ->
cover.alpha = (height - scrollY.toFloat()) / height binding.cover.alpha = (height - scrollY.toFloat()) / height
} }
} }
} }

View File

@ -1,7 +1,9 @@
package audio.funkwhale.ffa.fragments package audio.funkwhale.ffa.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -10,24 +12,41 @@ import androidx.transition.Slide
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.activities.MainActivity import audio.funkwhale.ffa.activities.MainActivity
import audio.funkwhale.ffa.adapters.AlbumsGridAdapter import audio.funkwhale.ffa.adapters.AlbumsGridAdapter
import audio.funkwhale.ffa.databinding.FragmentAlbumsGridBinding
import audio.funkwhale.ffa.repositories.AlbumsRepository import audio.funkwhale.ffa.repositories.AlbumsRepository
import audio.funkwhale.ffa.utils.Album import audio.funkwhale.ffa.utils.Album
import audio.funkwhale.ffa.utils.AppContext import audio.funkwhale.ffa.utils.AppContext
import kotlinx.android.synthetic.main.fragment_albums_grid.*
class AlbumsGridFragment : OtterFragment<Album, AlbumsGridAdapter>() { class AlbumsGridFragment : FFAFragment<Album, AlbumsGridAdapter>() {
override val viewRes = R.layout.fragment_albums_grid
override val recycler: RecyclerView get() = albums private var _binding: FragmentAlbumsGridBinding? = null
private val binding get() = _binding!!
override val recycler: RecyclerView get() = binding.albums
override val layoutManager get() = GridLayoutManager(context, 3) override val layoutManager get() = GridLayoutManager(context, 3)
override val alwaysRefresh = false override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
adapter = AlbumsGridAdapter(context, OnAlbumClickListener())
repository = AlbumsRepository(context) repository = AlbumsRepository(context)
} }
override fun onCreateView(
inflater: LayoutInflater,
parent: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAlbumsGridBinding.inflate(inflater)
adapter = AlbumsGridAdapter(inflater, OnAlbumClickListener())
swiper = binding.swiper
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
inner class OnAlbumClickListener : AlbumsGridAdapter.OnAlbumClickListener { inner class OnAlbumClickListener : AlbumsGridAdapter.OnAlbumClickListener {
override fun onClick(view: View?, album: Album) { override fun onClick(view: View?, album: Album) {
(context as? MainActivity)?.let { activity -> (context as? MainActivity)?.let { activity ->

View File

@ -2,7 +2,9 @@ package audio.funkwhale.ffa.fragments
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -12,19 +14,50 @@ import androidx.transition.Slide
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.activities.MainActivity import audio.funkwhale.ffa.activities.MainActivity
import audio.funkwhale.ffa.adapters.ArtistsAdapter import audio.funkwhale.ffa.adapters.ArtistsAdapter
import audio.funkwhale.ffa.databinding.FragmentArtistsBinding
import audio.funkwhale.ffa.repositories.ArtistsRepository import audio.funkwhale.ffa.repositories.ArtistsRepository
import audio.funkwhale.ffa.utils.AppContext import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.Artist import audio.funkwhale.ffa.utils.Artist
import audio.funkwhale.ffa.utils.onViewPager import audio.funkwhale.ffa.utils.onViewPager
import kotlinx.android.synthetic.main.fragment_artists.*
class ArtistsFragment : OtterFragment<Artist, ArtistsAdapter>() { class ArtistsFragment : FFAFragment<Artist, ArtistsAdapter>() {
override val viewRes = R.layout.fragment_artists
override val recycler: RecyclerView get() = artists private var _binding: FragmentArtistsBinding? = null
private val binding get() = _binding!!
override val recycler: RecyclerView get() = binding.artists
override val alwaysRefresh = false override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
repository = ArtistsRepository(context)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentArtistsBinding.inflate(inflater)
swiper = binding.swiper
adapter = ArtistsAdapter(inflater, context, OnArtistClickListener())
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object { companion object {
fun openAlbums(context: Context?, artist: Artist, fragment: Fragment? = null, art: String? = null) {
fun openAlbums(
context: Context?,
artist: Artist,
fragment: Fragment? = null,
art: String? = null
) {
(context as? MainActivity)?.let { (context as? MainActivity)?.let {
fragment?.let { fragment -> fragment?.let { fragment ->
fragment.onViewPager { fragment.onViewPager {
@ -57,13 +90,6 @@ class ArtistsFragment : OtterFragment<Artist, ArtistsAdapter>() {
} }
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = ArtistsAdapter(context, OnArtistClickListener())
repository = ArtistsRepository(context)
}
inner class OnArtistClickListener : ArtistsAdapter.OnArtistClickListener { inner class OnArtistClickListener : ArtistsAdapter.OnArtistClickListener {
override fun onClick(holder: View?, artist: Artist) { override fun onClick(holder: View?, artist: Artist) {
openAlbums(context, artist, fragment = this@ArtistsFragment) openAlbums(context, artist, fragment = this@ArtistsFragment)

View File

@ -5,30 +5,42 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.BrowseTabsAdapter import audio.funkwhale.ffa.adapters.BrowseTabsAdapter
import kotlinx.android.synthetic.main.fragment_browse.view.* import audio.funkwhale.ffa.databinding.FragmentBrowseBinding
class BrowseFragment : Fragment() { class BrowseFragment : Fragment() {
var adapter: BrowseTabsAdapter? = null
private var _binding: FragmentBrowseBinding? = null
private val binding get() = _binding!!
private var adapter: BrowseTabsAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
adapter = BrowseTabsAdapter(this, childFragmentManager) adapter = BrowseTabsAdapter(this, childFragmentManager)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
return inflater.inflate(R.layout.fragment_browse, container, false).apply { inflater: LayoutInflater,
tabs.setupWithViewPager(pager) container: ViewGroup?,
tabs.getTabAt(0)?.select() savedInstanceState: Bundle?
): View {
_binding = FragmentBrowseBinding.inflate(inflater)
return binding.root.apply {
binding.tabs.setupWithViewPager(binding.pager)
binding.tabs.getTabAt(0)?.select()
pager.adapter = adapter binding.pager.adapter = adapter
pager.offscreenPageLimit = 3 binding.pager.offscreenPageLimit = 3
} }
} }
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
fun selectTabAt(position: Int) { fun selectTabAt(position: Int) {
view?.tabs?.getTabAt(position)?.select() binding.tabs.getTabAt(position)?.select()
} }
} }

View File

@ -1,19 +1,17 @@
package audio.funkwhale.ffa.fragments package audio.funkwhale.ffa.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import audio.funkwhale.ffa.repositories.HttpUpstream import audio.funkwhale.ffa.repositories.HttpUpstream
import audio.funkwhale.ffa.repositories.Repository import audio.funkwhale.ffa.repositories.Repository
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.utils.*
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.android.synthetic.main.fragment_artists.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -21,7 +19,7 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() { abstract class FFAAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
var data: MutableList<D> = mutableListOf() var data: MutableList<D> = mutableListOf()
init { init {
@ -31,26 +29,22 @@ abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adap
abstract override fun getItemId(position: Int): Long abstract override fun getItemId(position: Int): Long
} }
abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() { abstract class FFAFragment<D : Any, A : FFAAdapter<D, *>>() : Fragment() {
companion object { companion object {
const val OFFSCREEN_PAGES = 20 const val OFFSCREEN_PAGES = 20
} }
abstract val viewRes: Int
abstract val recycler: RecyclerView abstract val recycler: RecyclerView
open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context) open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context)
open val alwaysRefresh = true open val alwaysRefresh = true
lateinit var repository: Repository<D, *> lateinit var repository: Repository<D, *>
lateinit var adapter: A lateinit var adapter: A
lateinit var swiper: SwipeRefreshLayout
private var moreLoading = false private var moreLoading = false
private var listener: Job? = null private var listener: Job? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(viewRes, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -77,7 +71,8 @@ abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
EventBus.get().collect { event -> EventBus.get().collect { event ->
if (event is Event.ListingsChanged) { if (event is Event.ListingsChanged) {
withContext(Main) { withContext(Main) {
swiper?.isRefreshing = true
swiper.isRefreshing = true
fetch(Repository.Origin.Network.origin) fetch(Repository.Origin.Network.origin)
} }
} }
@ -95,13 +90,13 @@ abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
swiper?.setOnRefreshListener { swiper.setOnRefreshListener {
fetch(Repository.Origin.Network.origin) fetch(Repository.Origin.Network.origin)
} }
} }
fun update() { fun update() {
swiper?.isRefreshing = true swiper.isRefreshing = true
fetch(Repository.Origin.Network.origin) fetch(Repository.Origin.Network.origin)
} }
@ -112,82 +107,84 @@ abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
if (!moreLoading && upstreams == Repository.Origin.Network.origin) { if (!moreLoading && upstreams == Repository.Origin.Network.origin) {
lifecycleScope.launch(Main) { lifecycleScope.launch(Main) {
swiper?.isRefreshing = true swiper.isRefreshing = true
} }
} }
moreLoading = true moreLoading = true
repository.fetch(upstreams, size).untilNetwork(lifecycleScope, IO) { data, isCache, _, hasMore -> repository.fetch(upstreams, size)
if (isCache && data.isEmpty()) { .untilNetwork(lifecycleScope, IO) { data, isCache, _, hasMore ->
moreLoading = false if (isCache && data.isEmpty()) {
return@untilNetwork fetch(Repository.Origin.Network.origin)
}
lifecycleScope.launch(Main) {
if (isCache) {
moreLoading = false moreLoading = false
adapter.data = data.toMutableList() return@untilNetwork fetch(Repository.Origin.Network.origin)
adapter.notifyDataSetChanged()
return@launch
} }
if (first) { lifecycleScope.launch(Main) {
adapter.data.clear() if (isCache) {
} moreLoading = false
onDataFetched(data) adapter.data = data.toMutableList()
adapter.notifyDataSetChanged()
adapter.data.addAll(data) return@launch
withContext(IO) {
try {
repository.cacheId?.let { cacheId ->
Cache.set(
context,
cacheId,
Gson().toJson(repository.cache(adapter.data)).toByteArray()
)
}
} catch (e: ConcurrentModificationException) {
} }
}
if (hasMore) { if (first) {
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream -> adapter.data.clear()
if (!isCache && upstream.behavior == HttpUpstream.Behavior.Progressive) { }
if (first || needsMoreOffscreenPages()) {
fetch(Repository.Origin.Network.origin, adapter.data.size) onDataFetched(data)
adapter.data.addAll(data)
withContext(IO) {
try {
repository.cacheId?.let { cacheId ->
Cache.set(
context,
cacheId,
Gson().toJson(repository.cache(adapter.data)).toByteArray()
)
}
} catch (e: ConcurrentModificationException) {
}
}
if (hasMore) {
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
if (!isCache && upstream.behavior == HttpUpstream.Behavior.Progressive) {
if (first || needsMoreOffscreenPages()) {
fetch(Repository.Origin.Network.origin, adapter.data.size)
} else {
moreLoading = false
}
} else { } else {
moreLoading = false moreLoading = false
} }
} else {
moreLoading = false
} }
} }
}
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream -> (repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
when (upstream.behavior) { when (upstream.behavior) {
HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper?.isRefreshing = false HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper?.isRefreshing =
HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper?.isRefreshing = false false
HttpUpstream.Behavior.Single -> if (!hasMore) swiper?.isRefreshing = false HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper.isRefreshing = false
} HttpUpstream.Behavior.Single -> if (!hasMore) swiper.isRefreshing = false
} }
when (first) {
true -> {
adapter.notifyDataSetChanged()
first = false
} }
false -> adapter.notifyItemRangeInserted(adapter.data.size, data.size) when (first) {
true -> {
adapter.notifyDataSetChanged()
first = false
}
false -> adapter.notifyItemRangeInserted(adapter.data.size, data.size)
}
} }
} }
}
} }
private fun needsMoreOffscreenPages(): Boolean { private fun needsMoreOffscreenPages(): Boolean {

View File

@ -1,35 +1,62 @@
package audio.funkwhale.ffa.fragments package audio.funkwhale.ffa.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.FavoritesAdapter import audio.funkwhale.ffa.adapters.FavoritesAdapter
import audio.funkwhale.ffa.databinding.FragmentFavoritesBinding
import audio.funkwhale.ffa.repositories.FavoritesRepository import audio.funkwhale.ffa.repositories.FavoritesRepository
import audio.funkwhale.ffa.repositories.TracksRepository import audio.funkwhale.ffa.repositories.TracksRepository
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.Event
import audio.funkwhale.ffa.utils.EventBus
import audio.funkwhale.ffa.utils.Request
import audio.funkwhale.ffa.utils.RequestBus
import audio.funkwhale.ffa.utils.Response
import audio.funkwhale.ffa.utils.Track
import audio.funkwhale.ffa.utils.getMetadata
import audio.funkwhale.ffa.utils.wait
import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.Download
import kotlinx.android.synthetic.main.fragment_favorites.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() { class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() {
override val viewRes = R.layout.fragment_favorites
override val recycler: RecyclerView get() = favorites private var _binding: FragmentFavoritesBinding? = null
private val binding get() = _binding!!
override val recycler: RecyclerView get() = binding.favorites
override val alwaysRefresh = false override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
adapter = FavoritesAdapter(layoutInflater, context, FavoriteListener())
adapter = FavoritesAdapter(context, FavoriteListener())
repository = FavoritesRepository(context) repository = FavoritesRepository(context)
watchEventBus() watchEventBus()
} }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentFavoritesBinding.inflate(inflater)
swiper = binding.swiper
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@ -44,7 +71,7 @@ class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
refreshDownloadedTracks() refreshDownloadedTracks()
} }
play.setOnClickListener { binding.play.setOnClickListener {
CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled())) CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
} }
} }
@ -83,12 +110,13 @@ class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
private suspend fun refreshDownloadedTrack(download: Download) { private suspend fun refreshDownloadedTrack(download: Download) {
if (download.state == Download.STATE_COMPLETED) { if (download.state == Download.STATE_COMPLETED) {
download.getMetadata()?.let { info -> download.getMetadata()?.let { info ->
adapter.data.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match -> adapter.data.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }
withContext(Main) { .toList().getOrNull(0)?.let { match ->
adapter.data[match.second].downloaded = true withContext(Main) {
adapter.notifyItemChanged(match.second) adapter.data[match.second].downloaded = true
adapter.notifyItemChanged(match.second)
}
} }
}
} }
} }
} }

View File

@ -7,16 +7,25 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.TracksAdapter import audio.funkwhale.ffa.adapters.TracksAdapter
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.databinding.PartialQueueBinding
import kotlinx.android.synthetic.main.partial_queue.* import audio.funkwhale.ffa.utils.Command
import kotlinx.android.synthetic.main.partial_queue.view.* import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.Event
import audio.funkwhale.ffa.utils.EventBus
import audio.funkwhale.ffa.utils.Request
import audio.funkwhale.ffa.utils.RequestBus
import audio.funkwhale.ffa.utils.Response
import audio.funkwhale.ffa.utils.wait
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class LandscapeQueueFragment : Fragment() { class LandscapeQueueFragment : Fragment() {
private var _binding: PartialQueueBinding? = null
private val binding get() = _binding!!
private var adapter: TracksAdapter? = null private var adapter: TracksAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -25,32 +34,42 @@ class LandscapeQueueFragment : Fragment() {
watchEventBus() watchEventBus()
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
return inflater.inflate(R.layout.partial_queue, container, false).apply { inflater: LayoutInflater,
adapter = TracksAdapter(context, fromQueue = true).also { container: ViewGroup?,
queue.layoutManager = LinearLayoutManager(context) savedInstanceState: Bundle?
queue.adapter = it ): View {
_binding = PartialQueueBinding.inflate(inflater)
return binding.root.apply {
adapter = TracksAdapter(layoutInflater, context, fromQueue = true).also {
binding.queue.layoutManager = LinearLayoutManager(context)
binding.queue.adapter = it
} }
} }
} }
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
queue?.visibility = View.GONE binding.queue.visibility = View.GONE
placeholder?.visibility = View.VISIBLE binding.placeholder.visibility = View.VISIBLE
queue_shuffle.setOnClickListener { binding.queueShuffle.setOnClickListener {
CommandBus.send(Command.ShuffleQueue) CommandBus.send(Command.ShuffleQueue)
} }
queue_save.setOnClickListener { binding.queueSave.setOnClickListener {
adapter?.data?.let { adapter?.data?.let {
CommandBus.send(Command.AddToPlaylist(it)) CommandBus.send(Command.AddToPlaylist(it))
} }
} }
queue_clear.setOnClickListener { binding.queueClear.setOnClickListener {
CommandBus.send(Command.ClearQueue) CommandBus.send(Command.ClearQueue)
} }
@ -65,11 +84,11 @@ class LandscapeQueueFragment : Fragment() {
it.notifyDataSetChanged() it.notifyDataSetChanged()
if (it.data.isEmpty()) { if (it.data.isEmpty()) {
queue?.visibility = View.GONE binding.queue.visibility = View.GONE
placeholder?.visibility = View.VISIBLE binding.placeholder.visibility = View.VISIBLE
} else { } else {
queue?.visibility = View.VISIBLE binding.queue.visibility = View.VISIBLE
placeholder?.visibility = View.GONE binding.placeholder.visibility = View.GONE
} }
} }
} }

View File

@ -2,27 +2,43 @@ package audio.funkwhale.ffa.fragments
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.PlaylistTracksAdapter import audio.funkwhale.ffa.adapters.PlaylistTracksAdapter
import audio.funkwhale.ffa.databinding.FragmentTracksBinding
import audio.funkwhale.ffa.repositories.FavoritesRepository import audio.funkwhale.ffa.repositories.FavoritesRepository
import audio.funkwhale.ffa.repositories.ManagementPlaylistsRepository import audio.funkwhale.ffa.repositories.ManagementPlaylistsRepository
import audio.funkwhale.ffa.repositories.PlaylistTracksRepository import audio.funkwhale.ffa.repositories.PlaylistTracksRepository
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.Playlist
import audio.funkwhale.ffa.utils.PlaylistTrack
import audio.funkwhale.ffa.utils.Request
import audio.funkwhale.ffa.utils.RequestBus
import audio.funkwhale.ffa.utils.Response
import audio.funkwhale.ffa.utils.Track
import audio.funkwhale.ffa.utils.maybeLoad
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import audio.funkwhale.ffa.utils.toast
import audio.funkwhale.ffa.utils.wait
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.fragment_tracks.*
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapter>() { class PlaylistTracksFragment : FFAFragment<PlaylistTrack, PlaylistTracksAdapter>() {
override val viewRes = R.layout.fragment_tracks
override val recycler: RecyclerView get() = tracks override val recycler: RecyclerView get() = binding.tracks
private var _binding: FragmentTracksBinding? = null
private val binding get() = _binding!!
lateinit var favoritesRepository: FavoritesRepository lateinit var favoritesRepository: FavoritesRepository
lateinit var playlistsRepository: ManagementPlaylistsRepository lateinit var playlistsRepository: ManagementPlaylistsRepository
@ -55,7 +71,7 @@ class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapte
albumCover = getString("albumCover") ?: "" albumCover = getString("albumCover") ?: ""
} }
adapter = PlaylistTracksAdapter(context, FavoriteListener(), PlaylistListener()) adapter = PlaylistTracksAdapter(layoutInflater, context, FavoriteListener(), PlaylistListener())
repository = PlaylistTracksRepository(context, albumId) repository = PlaylistTracksRepository(context, albumId)
favoritesRepository = FavoritesRepository(context) favoritesRepository = FavoritesRepository(context)
playlistsRepository = ManagementPlaylistsRepository(context) playlistsRepository = ManagementPlaylistsRepository(context)
@ -63,14 +79,29 @@ class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapte
watchEventBus() watchEventBus()
} }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentTracksBinding.inflate(layoutInflater)
swiper = binding.swiper
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
cover.visibility = View.INVISIBLE binding.cover.visibility = View.INVISIBLE
covers.visibility = View.VISIBLE binding.covers.visibility = View.VISIBLE
artist.text = "Playlist" binding.artist.text = "Playlist"
title.text = albumTitle binding.title.text = albumTitle
} }
override fun onResume() { override fun onResume() {
@ -85,27 +116,33 @@ class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapte
var coverHeight: Float? = null var coverHeight: Float? = null
scroller.setOnScrollChangeListener { _: View?, _: Int, scrollY: Int, _: Int, _: Int -> binding.scroller.setOnScrollChangeListener { _: View?, _: Int, scrollY: Int, _: Int, _: Int ->
if (coverHeight == null) { if (coverHeight == null) {
coverHeight = covers.measuredHeight.toFloat() coverHeight = binding.covers.measuredHeight.toFloat()
} }
covers.translationY = (scrollY / 2).toFloat() binding.covers.translationY = (scrollY / 2).toFloat()
coverHeight?.let { height -> coverHeight?.let { height ->
covers.alpha = (height - scrollY.toFloat()) / height binding.covers.alpha = (height - scrollY.toFloat()) / height
} }
} }
play.setOnClickListener { binding.play.setOnClickListener {
CommandBus.send(Command.ReplaceQueue(adapter.data.map { it.track }.shuffled())) CommandBus.send(Command.ReplaceQueue(adapter.data.map { it.track }.shuffled()))
context.toast("All tracks were added to your queue") context.toast("All tracks were added to your queue")
} }
context?.let { context -> context?.let { context ->
actions.setOnClickListener { binding.actions.setOnClickListener {
PopupMenu(context, actions, Gravity.START, R.attr.actionOverflowMenuStyle, 0).apply { PopupMenu(
context,
binding.actions,
Gravity.START,
R.attr.actionOverflowMenuStyle,
0
).apply {
inflate(R.menu.album) inflate(R.menu.album)
setOnMenuItemClickListener { setOnMenuItemClickListener {
@ -131,11 +168,11 @@ class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapte
override fun onDataFetched(data: List<PlaylistTrack>) { override fun onDataFetched(data: List<PlaylistTrack>) {
data.map { it.track.album }.toSet().map { it?.cover() }.take(4).forEachIndexed { index, url -> data.map { it.track.album }.toSet().map { it?.cover() }.take(4).forEachIndexed { index, url ->
val imageView = when (index) { val imageView = when (index) {
0 -> cover_top_left 0 -> binding.coverTopLeft
1 -> cover_top_right 1 -> binding.coverTopRight
2 -> cover_bottom_left 2 -> binding.coverBottomLeft
3 -> cover_bottom_right 3 -> binding.coverBottomRight
else -> cover_top_left else -> binding.coverTopLeft
} }
val corner = when (index) { val corner = when (index) {

View File

@ -1,7 +1,9 @@
package audio.funkwhale.ffa.fragments package audio.funkwhale.ffa.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.transition.Fade import androidx.transition.Fade
@ -9,23 +11,41 @@ import androidx.transition.Slide
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.activities.MainActivity import audio.funkwhale.ffa.activities.MainActivity
import audio.funkwhale.ffa.adapters.PlaylistsAdapter import audio.funkwhale.ffa.adapters.PlaylistsAdapter
import audio.funkwhale.ffa.databinding.FragmentPlaylistsBinding
import audio.funkwhale.ffa.repositories.PlaylistsRepository import audio.funkwhale.ffa.repositories.PlaylistsRepository
import audio.funkwhale.ffa.utils.AppContext import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.Playlist import audio.funkwhale.ffa.utils.Playlist
import kotlinx.android.synthetic.main.fragment_playlists.*
class PlaylistsFragment : OtterFragment<Playlist, PlaylistsAdapter>() { class PlaylistsFragment : FFAFragment<Playlist, PlaylistsAdapter>() {
override val viewRes = R.layout.fragment_playlists
override val recycler: RecyclerView get() = playlists override val recycler: RecyclerView get() = binding.playlists
override val alwaysRefresh = false override val alwaysRefresh = false
private var _binding: FragmentPlaylistsBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
adapter = PlaylistsAdapter(context, OnPlaylistClickListener()) adapter = PlaylistsAdapter(layoutInflater, context, OnPlaylistClickListener())
repository = PlaylistsRepository(context) repository = PlaylistsRepository(context)
} }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentPlaylistsBinding.inflate(layoutInflater)
swiper = binding.swiper
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
inner class OnPlaylistClickListener : PlaylistsAdapter.OnPlaylistClickListener { inner class OnPlaylistClickListener : PlaylistsAdapter.OnPlaylistClickListener {
override fun onClick(holder: View?, playlist: Playlist) { override fun onClick(holder: View?, playlist: Playlist) {
(context as? MainActivity)?.let { activity -> (context as? MainActivity)?.let { activity ->

View File

@ -10,19 +10,27 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.TracksAdapter import audio.funkwhale.ffa.adapters.TracksAdapter
import audio.funkwhale.ffa.databinding.FragmentQueueBinding
import audio.funkwhale.ffa.repositories.FavoritesRepository import audio.funkwhale.ffa.repositories.FavoritesRepository
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.Event
import audio.funkwhale.ffa.utils.EventBus
import audio.funkwhale.ffa.utils.Request
import audio.funkwhale.ffa.utils.RequestBus
import audio.funkwhale.ffa.utils.Response
import audio.funkwhale.ffa.utils.wait
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.android.synthetic.main.fragment_queue.*
import kotlinx.android.synthetic.main.fragment_queue.view.*
import kotlinx.android.synthetic.main.partial_queue.*
import kotlinx.android.synthetic.main.partial_queue.view.*
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class QueueFragment : BottomSheetDialogFragment() { class QueueFragment : BottomSheetDialogFragment() {
private var _binding: FragmentQueueBinding? = null
private val binding get() = _binding!!
private var adapter: TracksAdapter? = null private var adapter: TracksAdapter? = null
lateinit var favoritesRepository: FavoritesRepository lateinit var favoritesRepository: FavoritesRepository
@ -47,32 +55,42 @@ class QueueFragment : BottomSheetDialogFragment() {
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
return inflater.inflate(R.layout.fragment_queue, container, false).apply { inflater: LayoutInflater,
adapter = TracksAdapter(context, FavoriteListener(), fromQueue = true).also { container: ViewGroup?,
included.queue.layoutManager = LinearLayoutManager(context) savedInstanceState: Bundle?
included.queue.adapter = it ): View {
_binding = FragmentQueueBinding.inflate(inflater)
return binding.root.apply {
adapter = TracksAdapter(layoutInflater, context, FavoriteListener(), fromQueue = true).also {
binding.included.queue.layoutManager = LinearLayoutManager(context)
binding.included.queue.adapter = it
} }
} }
} }
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
included.queue?.visibility = View.GONE binding.included.queue.visibility = View.GONE
placeholder?.visibility = View.VISIBLE binding.included.placeholder?.visibility = View.VISIBLE
queue_shuffle.setOnClickListener { binding.included.queueShuffle.setOnClickListener {
CommandBus.send(Command.ShuffleQueue) CommandBus.send(Command.ShuffleQueue)
} }
queue_save.setOnClickListener { binding.included.queueSave.setOnClickListener {
adapter?.data?.let { adapter?.data?.let {
CommandBus.send(Command.AddToPlaylist(it)) CommandBus.send(Command.AddToPlaylist(it))
} }
} }
queue_clear.setOnClickListener { binding.included.queueClear.setOnClickListener {
CommandBus.send(Command.ClearQueue) CommandBus.send(Command.ClearQueue)
} }
@ -82,17 +100,17 @@ class QueueFragment : BottomSheetDialogFragment() {
private fun refresh() { private fun refresh() {
lifecycleScope.launch(Main) { lifecycleScope.launch(Main) {
RequestBus.send(Request.GetQueue).wait<Response.Queue>()?.let { response -> RequestBus.send(Request.GetQueue).wait<Response.Queue>()?.let { response ->
included?.let { included -> binding.included.let { included ->
adapter?.let { adapter?.let {
it.data = response.queue.toMutableList() it.data = response.queue.toMutableList()
it.notifyDataSetChanged() it.notifyDataSetChanged()
if (it.data.isEmpty()) { if (it.data.isEmpty()) {
included.queue?.visibility = View.GONE included.queue.visibility = View.GONE
placeholder?.visibility = View.VISIBLE binding.included.placeholder.visibility = View.VISIBLE
} else { } else {
included.queue?.visibility = View.VISIBLE included.queue.visibility = View.VISIBLE
placeholder?.visibility = View.GONE binding.included.placeholder.visibility = View.GONE
} }
} }
} }

View File

@ -1,32 +1,57 @@
package audio.funkwhale.ffa.fragments package audio.funkwhale.ffa.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.RadiosAdapter import audio.funkwhale.ffa.adapters.RadiosAdapter
import audio.funkwhale.ffa.databinding.FragmentRadiosBinding
import audio.funkwhale.ffa.repositories.RadiosRepository import audio.funkwhale.ffa.repositories.RadiosRepository
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.utils.Command
import kotlinx.android.synthetic.main.fragment_radios.* import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.Event
import audio.funkwhale.ffa.utils.EventBus
import audio.funkwhale.ffa.utils.Radio
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class RadiosFragment : OtterFragment<Radio, RadiosAdapter>() { class RadiosFragment : FFAFragment<Radio, RadiosAdapter>() {
override val viewRes = R.layout.fragment_radios
override val recycler: RecyclerView get() = radios override val recycler: RecyclerView get() = binding.radios
override val alwaysRefresh = false override val alwaysRefresh = false
private var _binding: FragmentRadiosBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
adapter = RadiosAdapter(context, lifecycleScope, RadioClickListener()) adapter = RadiosAdapter(layoutInflater, context, lifecycleScope, RadioClickListener())
repository = RadiosRepository(context) repository = RadiosRepository(context)
} }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentRadiosBinding.inflate(inflater)
swiper = binding.swiper
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
inner class RadioClickListener : RadiosAdapter.OnRadioClickListener { inner class RadioClickListener : RadiosAdapter.OnRadioClickListener {
override fun onClick(holder: RadiosAdapter.ViewHolder, radio: Radio) {
override fun onClick(holder: RadiosAdapter.RowRadioViewHolder, radio: Radio) {
holder.spin() holder.spin()
recycler.forEach { recycler.forEach {
it.isEnabled = false it.isEnabled = false
@ -39,11 +64,9 @@ class RadiosFragment : OtterFragment<Radio, RadiosAdapter>() {
EventBus.get().collect { message -> EventBus.get().collect { message ->
when (message) { when (message) {
is Event.RadioStarted -> is Event.RadioStarted ->
if (radios != null) { recycler.forEach {
recycler.forEach { it.isEnabled = true
it.isEnabled = true it.isClickable = true
it.isClickable = true
}
} }
} }
} }

View File

@ -11,12 +11,16 @@ import android.widget.TextView
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.FragmentTrackInfoDetailsBinding
import audio.funkwhale.ffa.utils.Track import audio.funkwhale.ffa.utils.Track
import audio.funkwhale.ffa.utils.mustNormalizeUrl import audio.funkwhale.ffa.utils.mustNormalizeUrl
import audio.funkwhale.ffa.utils.toDurationString import audio.funkwhale.ffa.utils.toDurationString
import kotlinx.android.synthetic.main.fragment_track_info_details.*
class TrackInfoDetailsFragment : DialogFragment() { class TrackInfoDetailsFragment : DialogFragment() {
private var _binding: FragmentTrackInfoDetailsBinding? = null
private val binding get() = _binding!!
companion object { companion object {
fun new(track: Track): TrackInfoDetailsFragment { fun new(track: Track): TrackInfoDetailsFragment {
return TrackInfoDetailsFragment().apply { return TrackInfoDetailsFragment().apply {
@ -27,7 +31,8 @@ class TrackInfoDetailsFragment : DialogFragment() {
"trackCopyright" to track.copyright, "trackCopyright" to track.copyright,
"trackLicense" to track.license, "trackLicense" to track.license,
"trackPosition" to track.position, "trackPosition" to track.position,
"trackDuration" to track.bestUpload()?.duration?.toLong()?.let { toDurationString(it, showSeconds = true) }, "trackDuration" to track.bestUpload()?.duration?.toLong()
?.let { toDurationString(it, showSeconds = true) },
"trackBitrate" to track.bestUpload()?.bitrate?.let { "${it / 1000} Kbps" }, "trackBitrate" to track.bestUpload()?.bitrate?.let { "${it / 1000} Kbps" },
"trackInstance" to track.bestUpload()?.listen_url?.let { Uri.parse(mustNormalizeUrl(it)).authority } "trackInstance" to track.bestUpload()?.listen_url?.let { Uri.parse(mustNormalizeUrl(it)).authority }
) )
@ -53,14 +58,29 @@ class TrackInfoDetailsFragment : DialogFragment() {
properties.add(Pair(R.string.track_info_details_track_copyright, getString("trackCopyright"))) properties.add(Pair(R.string.track_info_details_track_copyright, getString("trackCopyright")))
properties.add(Pair(R.string.track_info_details_track_license, getString("trackLicense"))) properties.add(Pair(R.string.track_info_details_track_license, getString("trackLicense")))
properties.add(Pair(R.string.track_info_details_track_duration, getString("trackDuration"))) properties.add(Pair(R.string.track_info_details_track_duration, getString("trackDuration")))
properties.add(Pair(R.string.track_info_details_track_position, getInt("trackPosition").toString())) properties.add(
Pair(
R.string.track_info_details_track_position,
getInt("trackPosition").toString()
)
)
properties.add(Pair(R.string.track_info_details_track_bitrate, getString("trackBitrate"))) properties.add(Pair(R.string.track_info_details_track_bitrate, getString("trackBitrate")))
properties.add(Pair(R.string.track_info_details_track_instance, getString("trackInstance"))) properties.add(Pair(R.string.track_info_details_track_instance, getString("trackInstance")))
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
return inflater.inflate(R.layout.fragment_track_info_details, container, false) inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentTrackInfoDetailsBinding.inflate(inflater)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -75,11 +95,17 @@ class TrackInfoDetailsFragment : DialogFragment() {
val valueTextView = TextView(context).apply { val valueTextView = TextView(context).apply {
text = value ?: "N/A" text = value ?: "N/A"
setTextAppearance(R.style.AppTheme_TrackDetailsValue) setTextAppearance(R.style.AppTheme_TrackDetailsValue)
setPadding(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16f, resources.displayMetrics).toInt()) setPadding(
0,
0,
0,
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16f, resources.displayMetrics)
.toInt()
)
} }
infos.addView(labelTextView) binding.infos.addView(labelTextView)
infos.addView(valueTextView) binding.infos.addView(valueTextView)
} }
} }
} }

View File

@ -4,13 +4,16 @@ import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffColorFilter
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.TracksAdapter import audio.funkwhale.ffa.adapters.TracksAdapter
import audio.funkwhale.ffa.databinding.FragmentTracksBinding
import audio.funkwhale.ffa.repositories.FavoritedRepository import audio.funkwhale.ffa.repositories.FavoritedRepository
import audio.funkwhale.ffa.repositories.FavoritesRepository import audio.funkwhale.ffa.repositories.FavoritesRepository
import audio.funkwhale.ffa.repositories.TracksRepository import audio.funkwhale.ffa.repositories.TracksRepository
@ -32,25 +35,21 @@ import com.google.android.exoplayer2.offline.Download
import com.preference.PowerPreference import com.preference.PowerPreference
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.fragment_tracks.actions
import kotlinx.android.synthetic.main.fragment_tracks.artist
import kotlinx.android.synthetic.main.fragment_tracks.cover
import kotlinx.android.synthetic.main.fragment_tracks.play
import kotlinx.android.synthetic.main.fragment_tracks.scroller
import kotlinx.android.synthetic.main.fragment_tracks.title
import kotlinx.android.synthetic.main.fragment_tracks.tracks
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class TracksFragment : OtterFragment<Track, TracksAdapter>() { class TracksFragment : FFAFragment<Track, TracksAdapter>() {
override val viewRes = R.layout.fragment_tracks
override val recycler: RecyclerView get() = tracks
lateinit var favoritesRepository: FavoritesRepository override val recycler: RecyclerView get() = binding.tracks
lateinit var favoritedRepository: FavoritedRepository
private var _binding: FragmentTracksBinding? = null
private val binding get() = _binding!!
private lateinit var favoritesRepository: FavoritesRepository
private lateinit var favoritedRepository: FavoritedRepository
private var albumId = 0 private var albumId = 0
private var albumArtist = "" private var albumArtist = ""
@ -80,7 +79,7 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
albumCover = getString("albumCover") ?: "" albumCover = getString("albumCover") ?: ""
} }
adapter = TracksAdapter(context, FavoriteListener()) adapter = TracksAdapter(layoutInflater, context, FavoriteListener())
repository = TracksRepository(context, albumId) repository = TracksRepository(context, albumId)
favoritesRepository = FavoritesRepository(context) favoritesRepository = FavoritesRepository(context)
favoritedRepository = FavoritedRepository(context) favoritedRepository = FavoritedRepository(context)
@ -92,8 +91,8 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
when { when {
data.all { it.downloaded } -> { data.all { it.downloaded } -> {
title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0) binding.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0)
title.compoundDrawables.forEach { binding.title.compoundDrawables.forEach {
it?.colorFilter = it?.colorFilter =
PorterDuffColorFilter( PorterDuffColorFilter(
requireContext().getColor(R.color.downloaded), requireContext().getColor(R.color.downloaded),
@ -102,8 +101,8 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
} }
} }
data.all { it.cached } -> { data.all { it.cached } -> {
title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0) binding.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0)
title.compoundDrawables.forEach { binding.title.compoundDrawables.forEach {
it?.colorFilter = it?.colorFilter =
PorterDuffColorFilter( PorterDuffColorFilter(
requireContext().getColor(R.color.cached), requireContext().getColor(R.color.cached),
@ -112,11 +111,26 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
} }
} }
else -> { else -> {
title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0) binding.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
} }
} }
} }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentTracksBinding.inflate(inflater)
swiper = binding.swiper
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -126,10 +140,10 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
.fit() .fit()
.centerCrop() .centerCrop()
.transform(RoundedCornersTransformation(16, 0)) .transform(RoundedCornersTransformation(16, 0))
.into(cover) .into(binding.cover)
artist.text = albumArtist binding.artist.text = albumArtist
title.text = albumTitle binding.title.text = albumTitle
} }
override fun onResume() { override fun onResume() {
@ -146,24 +160,24 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
var coverHeight: Float? = null var coverHeight: Float? = null
scroller.setOnScrollChangeListener { _: View?, _: Int, scrollY: Int, _: Int, _: Int -> binding.scroller.setOnScrollChangeListener { _: View?, _: Int, scrollY: Int, _: Int, _: Int ->
if (coverHeight == null) { if (coverHeight == null) {
coverHeight = cover.measuredHeight.toFloat() coverHeight = binding.cover.measuredHeight.toFloat()
} }
cover.translationY = (scrollY / 2).toFloat() binding.cover.translationY = (scrollY / 2).toFloat()
coverHeight?.let { height -> coverHeight?.let { height ->
cover.alpha = (height - scrollY.toFloat()) / height binding.cover.alpha = (height - scrollY.toFloat()) / height
} }
} }
when (PowerPreference.getDefaultFile().getString("play_order")) { when (PowerPreference.getDefaultFile().getString("play_order")) {
"in_order" -> play.text = getString(R.string.playback_play) "in_order" -> binding.play.text = getString(R.string.playback_play)
else -> play.text = getString(R.string.playback_shuffle) else -> binding.play.text = getString(R.string.playback_shuffle)
} }
play.setOnClickListener { binding.play.setOnClickListener {
when (PowerPreference.getDefaultFile().getString("play_order")) { when (PowerPreference.getDefaultFile().getString("play_order")) {
"in_order" -> CommandBus.send(Command.ReplaceQueue(adapter.data)) "in_order" -> CommandBus.send(Command.ReplaceQueue(adapter.data))
else -> CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled())) else -> CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
@ -173,8 +187,14 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
} }
context?.let { context -> context?.let { context ->
actions.setOnClickListener { binding.actions.setOnClickListener {
PopupMenu(context, actions, Gravity.START, R.attr.actionOverflowMenuStyle, 0).apply { PopupMenu(
context,
binding.actions,
Gravity.START,
R.attr.actionOverflowMenuStyle,
0
).apply {
inflate(R.menu.album) inflate(R.menu.album)
menu.findItem(R.id.play_secondary)?.let { item -> menu.findItem(R.id.play_secondary)?.let { item ->

View File

@ -5,13 +5,14 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.GestureDetector import android.view.GestureDetector
import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.PartialNowPlayingBinding
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import kotlinx.android.synthetic.main.partial_now_playing.view.*
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
@ -20,6 +21,9 @@ class NowPlayingView : MaterialCardView {
var gestureDetector: GestureDetector? = null var gestureDetector: GestureDetector? = null
var gestureDetectorCallback: OnGestureDetection? = null var gestureDetectorCallback: OnGestureDetection? = null
private val binding =
PartialNowPlayingBinding.inflate(LayoutInflater.from(context), this, true)
constructor(context: Context) : super(context) { constructor(context: Context) : super(context) {
activity = context activity = context
} }
@ -35,7 +39,10 @@ class NowPlayingView : MaterialCardView {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec) super.onMeasure(widthMeasureSpec, heightMeasureSpec)
now_playing_root.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED)) binding.nowPlayingRoot.measure(
widthMeasureSpec,
MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED)
)
} }
override fun onVisibilityChanged(changedView: View, visibility: Int) { override fun onVisibilityChanged(changedView: View, visibility: Int) {
@ -55,7 +62,7 @@ class NowPlayingView : MaterialCardView {
gestureDetectorCallback?.onUp() gestureDetectorCallback?.onUp()
} }
} }
performClick()
ret ret
} }
@ -93,7 +100,7 @@ class NowPlayingView : MaterialCardView {
TypedValue.complexToDimensionPixelSize(it.data, resources.displayMetrics) TypedValue.complexToDimensionPixelSize(it.data, resources.displayMetrics)
} }
maxHeight = now_playing_details.measuredHeight + (2 * maxMargin) maxHeight = binding.nowPlayingDetails.measuredHeight + (2 * maxMargin)
} }
override fun onDown(e: MotionEvent): Boolean { override fun onDown(e: MotionEvent): Boolean {
@ -120,7 +127,12 @@ class NowPlayingView : MaterialCardView {
} }
} }
override fun onFling(firstMotionEvent: MotionEvent?, secondMotionEvent: MotionEvent?, velocityX: Float, velocityY: Float): Boolean { override fun onFling(
firstMotionEvent: MotionEvent?,
secondMotionEvent: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
isScrolling = false isScrolling = false
layoutParams.let { layoutParams.let {
@ -138,7 +150,12 @@ class NowPlayingView : MaterialCardView {
return true return true
} }
override fun onScroll(firstMotionEvent: MotionEvent, secondMotionEvent: MotionEvent, distanceX: Float, distanceY: Float): Boolean { override fun onScroll(
firstMotionEvent: MotionEvent,
secondMotionEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
isScrolling = true isScrolling = true
layoutParams.let { layoutParams.let {
@ -146,10 +163,10 @@ class NowPlayingView : MaterialCardView {
val progress = (newHeight - minHeight) / (maxHeight - minHeight) val progress = (newHeight - minHeight) / (maxHeight - minHeight)
val newMargin = maxMargin - (maxMargin * progress) val newMargin = maxMargin - (maxMargin * progress)
(layoutParams as? MarginLayoutParams)?.let { (layoutParams as? MarginLayoutParams)?.let { params ->
it.marginStart = newMargin.toInt() params.marginStart = newMargin.toInt()
it.marginEnd = newMargin.toInt() params.marginEnd = newMargin.toInt()
it.bottomMargin = newMargin.toInt() params.bottomMargin = newMargin.toInt()
} }
layoutParams = layoutParams.apply { layoutParams = layoutParams.apply {
@ -166,9 +183,9 @@ class NowPlayingView : MaterialCardView {
} }
} }
summary.alpha = 1f - progress binding.summary.alpha = 1f - progress
summary.layoutParams = summary.layoutParams.apply { binding.summary.layoutParams = binding.summary.layoutParams.apply {
height = (minHeight * (1f - progress)).toInt() height = (minHeight * (1f - progress)).toInt()
} }
} }
@ -223,9 +240,9 @@ class NowPlayingView : MaterialCardView {
height = newHeight height = newHeight
summary.alpha = 1f - progress binding.summary.alpha = 1f - progress
summary.layoutParams = summary.layoutParams.apply { binding.summary.layoutParams = binding.summary.layoutParams.apply {
height = (minHeight * (1f - progress)).toInt() height = (minHeight * (1f - progress)).toInt()
} }
} }

View File

@ -27,7 +27,9 @@
tools:alpha="1" tools:alpha="1"
tools:visibility="visible"> tools:visibility="visible">
<include layout="@layout/partial_now_playing" /> <include
android:id="@+id/now_playing_container"
layout="@layout/partial_now_playing" />
</audio.funkwhale.ffa.views.NowPlayingView> </audio.funkwhale.ffa.views.NowPlayingView>

View File

@ -1,56 +1,56 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginVertical="5dp"
android:background="@drawable/ripple"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="8dp"
android:transitionGroup="true"
tools:showIn="@layout/fragment_albums">
<audio.funkwhale.ffa.views.SquareImageView
android:id="@+id/art"
android:layout_width="48dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
tools:src="@tools:sample/avatars" />
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_marginHorizontal="8dp"
android:orientation="vertical"> android:layout_marginVertical="5dp"
android:background="@drawable/ripple"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="8dp"
android:transitionGroup="true"
tools:showIn="@layout/fragment_albums">
<TextView <audio.funkwhale.ffa.views.SquareImageView
android:id="@+id/title" android:id="@+id/art"
style="@style/AppTheme.ItemTitle" android:layout_width="48dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
tools:src="@tools:sample/avatars" />
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="4dp" android:layout_weight="1"
android:ellipsize="end" android:orientation="vertical">
android:lines="1"
tools:text="Absolution" />
<TextView <TextView
android:id="@+id/artist" android:id="@+id/title"
android:layout_width="match_parent" style="@style/AppTheme.ItemTitle"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:ellipsize="end" android:layout_height="wrap_content"
android:lines="1" android:layout_marginBottom="4dp"
tools:text="Muse" /> android:ellipsize="end"
android:lines="1"
tools:text="Absolution" />
<TextView
android:id="@+id/artist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
tools:text="Muse" />
</LinearLayout> </LinearLayout>
<TextView <TextView
android:id="@+id/release_date" android:id="@+id/release_date"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_weight="0" android:layout_weight="0"
android:background="@drawable/pill" /> android:background="@drawable/pill" />
</LinearLayout> </LinearLayout>