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 {
id("com.android.application")
id("kotlin-android")
id("kotlin-android-extensions")
id("org.jlleitschuh.gradle.ktlint") version "8.1.0"
id("com.gladed.androidgitversion") version "0.4.14"
@ -34,6 +33,10 @@ android {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildFeatures {
viewBinding = true
}
buildToolsVersion = "29.0.3"
compileSdkVersion(29)

View File

@ -4,36 +4,38 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
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 kotlinx.android.synthetic.main.activity_downloads.downloads
import kotlinx.coroutines.Dispatchers.Default
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
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.adapters.DownloadsAdapter
import audio.funkwhale.ffa.utils.Event
import audio.funkwhale.ffa.utils.EventBus
import audio.funkwhale.ffa.utils.getMetadata
class DownloadsActivity : AppCompatActivity() {
lateinit var adapter: DownloadsAdapter
private lateinit var adapter: DownloadsAdapter
private lateinit var binding: ActivityDownloadsBinding
override fun onCreate(savedInstanceState: Bundle?) {
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)
downloads.layoutManager = LinearLayoutManager(this)
downloads.adapter = it
binding.downloads.layoutManager = LinearLayoutManager(this)
binding.downloads.adapter = it
}
lifecycleScope.launch(Default) {
@ -80,17 +82,18 @@ class DownloadsActivity : AppCompatActivity() {
private suspend fun refreshTrack(download: Download) {
download.getMetadata()?.let { info ->
adapter.downloads.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
if (download.state != info.download?.state) {
withContext(Main) {
adapter.downloads[match.second] = info.apply {
this.download = download
}
adapter.downloads.withIndex().associate { it.value to it.index }
.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
if (download.state != info.download?.state) {
withContext(Main) {
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
download.getMetadata()?.let { info ->
adapter.downloads.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
if (download.state == Download.STATE_DOWNLOADING && download.percentDownloaded != info.download?.percentDownloaded ?: 0) {
withContext(Main) {
adapter.downloads[match.second] = info.apply {
this.download = download
}
adapter.downloads.withIndex().associate { it.value to it.index }
.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
if (download.state == Download.STATE_DOWNLOADING && download.percentDownloaded != info.download?.percentDownloaded ?: 0) {
withContext(Main) {
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.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R
import kotlinx.android.synthetic.main.activity_licences.*
import kotlinx.android.synthetic.main.row_licence.view.*
import audio.funkwhale.ffa.databinding.ActivityLicencesBinding
import audio.funkwhale.ffa.databinding.RowLicenceBinding
class LicencesActivity : AppCompatActivity() {
private lateinit var binding: ActivityLicencesBinding
data class Licence(val name: String, val licence: String, val url: String)
interface OnLicenceClickListener {
@ -23,15 +24,18 @@ class LicencesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_licences)
binding = ActivityLicencesBinding.inflate(layoutInflater)
setContentView(binding.root)
LicencesAdapter(OnLicenceClick()).also {
licences.layoutManager = LinearLayoutManager(this)
licences.adapter = it
binding.licences.layoutManager = LinearLayoutManager(this)
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(
Licence(
"ExoPlayer",
@ -73,10 +77,9 @@ class LicencesActivity : AppCompatActivity() {
override fun getItemCount() = licences.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(this@LicencesActivity).inflate(R.layout.row_licence, parent, false)
return ViewHolder(view).also {
view.setOnClickListener(it)
val binding = RowLicenceBinding.inflate(layoutInflater)
return ViewHolder(binding).also {
binding.root.setOnClickListener(it)
}
}
@ -87,9 +90,10 @@ class LicencesActivity : AppCompatActivity() {
holder.licence.text = item.licence
}
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
val name = view.name
val licence = view.licence
inner class ViewHolder(binding: RowLicenceBinding) : RecyclerView.ViewHolder(binding.root),
View.OnClickListener {
val name = binding.name
val licence = binding.licence
override fun onClick(view: View?) {
listener.onClick(licences[layoutPosition].url)

View File

@ -10,6 +10,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.doOnLayout
import androidx.lifecycle.lifecycleScope
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.ActivityLoginBinding
import audio.funkwhale.ffa.fragments.LoginDialog
import audio.funkwhale.ffa.utils.AppContext
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.google.gson.Gson
import com.preference.PowerPreference
import kotlinx.android.synthetic.main.activity_login.*
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
data class FwCredentials(val token: String, val non_field_errors: List<String>?)
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
limitContainerWidth()
}
@ -37,40 +42,40 @@ class LoginActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
anonymous?.setOnCheckedChangeListener { _, isChecked ->
binding.anonymous.setOnCheckedChangeListener { _, isChecked ->
val state = when (isChecked) {
true -> View.GONE
false -> View.VISIBLE
}
username_field.visibility = state
password_field.visibility = state
binding.usernameField.visibility = state
binding.passwordField.visibility = state
}
login?.setOnClickListener {
var hostname = hostname.text.toString().trim()
val username = username.text.toString()
val password = password.text.toString()
binding.login?.setOnClickListener {
var hostname = binding.hostname.text.toString().trim()
val username = binding.username.text.toString()
val password = binding.password.text.toString()
try {
if (hostname.isEmpty()) throw Exception(getString(R.string.login_error_hostname))
Uri.parse(hostname).apply {
if (!cleartext.isChecked && scheme == "http") {
if (!binding.cleartext.isChecked && scheme == "http") {
throw Exception(getString(R.string.login_error_hostname_https))
}
if (scheme == null) {
hostname = when (cleartext.isChecked) {
hostname = when (binding.cleartext.isChecked) {
true -> "http://$hostname"
false -> "https://$hostname"
}
}
}
hostname_field.error = ""
binding.hostnameField.error = ""
when (anonymous.isChecked) {
when (binding.anonymous.isChecked) {
false -> authedLogin(hostname, username, password)
true -> anonymousLogin(hostname)
}
@ -79,7 +84,7 @@ class LoginActivity : AppCompatActivity() {
if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
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)
hostname_field.error = null
username_field.error = null
binding.hostnameField.error = null
binding.usernameField.error = null
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 {
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)
else e.message
hostname_field.error = message
binding.hostnameField.error = message
}
}
}
@ -177,7 +182,7 @@ class LoginActivity : AppCompatActivity() {
is Result.Failure -> {
dialog.dismiss()
hostname_field.error = result.error.localizedMessage
binding.hostnameField.error = result.error.localizedMessage
}
}
} catch (e: Exception) {
@ -187,20 +192,20 @@ class LoginActivity : AppCompatActivity() {
if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
else e.message
hostname_field.error = message
binding.hostnameField.error = message
}
}
}
private fun limitContainerWidth() {
container.doOnLayout {
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && container.width >= 1440) {
container.layoutParams.width = 1440
binding.container.doOnLayout {
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && binding.container.width >= 1440) {
binding.container.layoutParams.width = 1440
} 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.lifecycle.Lifecycle
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.databinding.ActivityMainBinding
import audio.funkwhale.ffa.fragments.*
import audio.funkwhale.ffa.playback.MediaControlsManager
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.utils.*
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() {
enum class ResultCode(val code: Int) {
@ -58,13 +56,18 @@ class MainActivity : AppCompatActivity() {
private val favoritedRepository = FavoritedRepository(this)
private var menu: Menu? = null
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AppContext.init(this)
setContentView(R.layout.activity_main)
setSupportActionBar(appbar)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.appbar)
when (intent.action) {
MediaControlsManager.NOTIFICATION_ACTION_OPEN_QUEUE.toString() -> launchDialog(QueueFragment())
@ -81,9 +84,9 @@ class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
(container as? DisableableFrameLayout)?.setShouldRegisterTouch { _ ->
if (now_playing.isOpened()) {
now_playing.close()
(binding.container as? DisableableFrameLayout)?.setShouldRegisterTouch { _ ->
if (binding.nowPlaying.isOpened()) {
binding.nowPlaying.close()
return@setShouldRegisterTouch false
}
@ -102,48 +105,51 @@ class MainActivity : AppCompatActivity() {
Userinfo.get()
}
now_playing_toggle.setOnClickListener {
CommandBus.send(Command.ToggleState)
}
with(binding) {
now_playing_next.setOnClickListener {
CommandBus.send(Command.NextTrack)
}
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))
}
nowPlayingContainer?.nowPlayingToggle?.setOnClickListener {
CommandBus.send(Command.ToggleState)
}
})
landscape_queue?.let {
supportFragmentManager.beginTransaction()
.replace(R.id.landscape_queue, LandscapeQueueFragment()).commit()
nowPlayingContainer?.nowPlayingNext?.setOnClickListener {
CommandBus.send(Command.NextTrack)
}
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() {
if (now_playing.isOpened()) {
now_playing.close()
if (binding.nowPlaying.isOpened()) {
binding.nowPlaying.close()
return
}
@ -173,7 +179,7 @@ class MainActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
now_playing.close()
binding.nowPlaying.close()
(supportFragmentManager.fragments.last() as? BrowseFragment)?.let {
it.selectTabAt(0)
@ -305,29 +311,29 @@ class MainActivity : AppCompatActivity() {
is Event.Buffering -> {
when (message.value) {
true -> now_playing_buffering.visibility = View.VISIBLE
false -> now_playing_buffering.visibility = View.GONE
true -> binding.nowPlayingContainer?.nowPlayingBuffering?.visibility = View.VISIBLE
false -> binding.nowPlayingContainer?.nowPlayingBuffering?.visibility = View.GONE
}
}
is Event.PlaybackStopped -> {
if (now_playing.visibility == View.VISIBLE) {
(container.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
if (binding.nowPlaying.visibility == View.VISIBLE) {
(binding.container.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
it.bottomMargin = it.bottomMargin / 2
}
landscape_queue?.let { landscape_queue ->
binding.landscapeQueue?.let { landscape_queue ->
(landscape_queue.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
it.bottomMargin = it.bottomMargin / 2
}
}
now_playing.animate()
binding.nowPlaying.animate()
.alpha(0.0f)
.setDuration(400)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animator: Animator?) {
now_playing.visibility = View.GONE
binding.nowPlaying.visibility = View.GONE
}
})
.start()
@ -339,13 +345,14 @@ class MainActivity : AppCompatActivity() {
is Event.StateChanged -> {
when (message.playing) {
true -> {
now_playing_toggle.icon = getDrawable(R.drawable.pause)
now_playing_details_toggle.icon = getDrawable(R.drawable.pause)
binding.nowPlayingContainer?.nowPlayingToggle?.icon = getDrawable(R.drawable.pause)
binding.nowPlayingContainer?.nowPlayingToggle?.icon = getDrawable(R.drawable.pause)
}
false -> {
now_playing_toggle.icon = getDrawable(R.drawable.play)
now_playing_details_toggle.icon = getDrawable(R.drawable.play)
binding.nowPlayingContainer?.nowPlayingToggle?.icon = getDrawable(R.drawable.play)
binding.nowPlayingContainer?.nowPlayingDetailsToggle?.icon =
getDrawable(R.drawable.play)
}
}
}
@ -369,9 +376,13 @@ class MainActivity : AppCompatActivity() {
is Command.StartService -> {
Build.VERSION_CODES.O.onApi(
{
startForegroundService(Intent(this@MainActivity, PlayerService::class.java).apply {
putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString())
})
startForegroundService(
Intent(
this@MainActivity,
PlayerService::class.java
).apply {
putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString())
})
},
{
startService(Intent(this@MainActivity, PlayerService::class.java).apply {
@ -384,7 +395,12 @@ class MainActivity : AppCompatActivity() {
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
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) {
ProgressBus.get().collect { (current, duration, percent) ->
now_playing_progress.progress = percent
now_playing_details_progress.progress = percent
binding.nowPlayingContainer?.nowPlayingProgress?.progress = percent
binding.nowPlayingContainer?.nowPlayingDetailsProgress?.progress = percent
val currentMins = (current / 1000) / 60
val currentSecs = (current / 1000) % 60
@ -401,59 +417,61 @@ class MainActivity : AppCompatActivity() {
val durationMins = duration / 60
val durationSecs = duration % 60
now_playing_details_progress_current.text = "%02d:%02d".format(currentMins, currentSecs)
now_playing_details_progress_duration.text = "%02d:%02d".format(durationMins, durationSecs)
binding.nowPlayingContainer?.nowPlayingDetailsProgressCurrent?.text =
"%02d:%02d".format(currentMins, currentSecs)
binding.nowPlayingContainer?.nowPlayingDetailsProgressDuration?.text =
"%02d:%02d".format(durationMins, durationSecs)
}
}
}
private fun refreshCurrentTrack(track: Track?) {
track?.let {
if (now_playing.visibility == View.GONE) {
now_playing.visibility = View.VISIBLE
now_playing.alpha = 0f
if (binding.nowPlaying.visibility == View.GONE) {
binding.nowPlaying.visibility = View.VISIBLE
binding.nowPlaying.alpha = 0f
now_playing.animate()
binding.nowPlaying.animate()
.alpha(1.0f)
.setDuration(400)
.setListener(null)
.start()
(container.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
(binding.container.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
it.bottomMargin = it.bottomMargin * 2
}
landscape_queue?.let { landscape_queue ->
binding.landscapeQueue?.let { landscape_queue ->
(landscape_queue.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
it.bottomMargin = it.bottomMargin * 2
}
}
}
now_playing_title.text = track.title
now_playing_album.text = track.artist.name
now_playing_toggle.icon = getDrawable(R.drawable.pause)
binding.nowPlayingContainer?.nowPlayingTitle?.text = track.title
binding.nowPlayingContainer?.nowPlayingAlbum?.text = track.artist.name
binding.nowPlayingContainer?.nowPlayingToggle?.icon = getDrawable(R.drawable.pause)
now_playing_details_title.text = track.title
now_playing_details_artist.text = track.artist.name
now_playing_details_toggle.icon = getDrawable(R.drawable.pause)
binding.nowPlayingContainer?.nowPlayingDetailsTitle?.text = track.title
binding.nowPlayingContainer?.nowPlayingDetailsArtist?.text = track.artist.name
binding.nowPlayingContainer?.nowPlayingDetailsToggle?.icon = getDrawable(R.drawable.pause)
Picasso.get()
.maybeLoad(maybeNormalizeUrl(track.album?.cover?.urls?.original))
.fit()
.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()
.maybeLoad(maybeNormalizeUrl(track.album?.cover()))
.fit()
.centerCrop()
.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) {
val width = DisplayMetrics().apply {
windowManager.defaultDisplay.getMetrics(this)
@ -469,12 +487,12 @@ class MainActivity : AppCompatActivity() {
}
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)
now_playing_details_repeat.setOnClickListener {
@ -484,11 +502,11 @@ class MainActivity : AppCompatActivity() {
}
}
now_playing_details_info?.let { now_playing_details_info ->
now_playing_details_info.setOnClickListener {
binding.nowPlayingContainer?.nowPlayingDetailsInfo?.let { nowPlayingDetailsInfo ->
nowPlayingDetailsInfo.setOnClickListener {
PopupMenu(
this@MainActivity,
now_playing_details_info,
nowPlayingDetailsInfo,
Gravity.START,
R.attr.actionOverflowMenuStyle,
0
@ -507,7 +525,7 @@ class MainActivity : AppCompatActivity() {
.show(supportFragmentManager, "dialog")
}
now_playing.close()
binding.nowPlaying.close()
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, _, _, _ ->
lifecycleScope.launch(Main) {
track.favorite = favorites.contains(track.id)
@ -547,7 +565,7 @@ class MainActivity : AppCompatActivity() {
favoriteRepository.fetch(Repository.Origin.Network.origin)
}
now_playing_details_add_to_playlist.setOnClickListener {
binding.nowPlayingContainer?.nowPlayingDetailsAddToPlaylist?.setOnClickListener {
CommandBus.send(Command.AddToPlaylist(listOf(track)))
}
}
@ -560,14 +578,14 @@ class MainActivity : AppCompatActivity() {
0 -> {
Cache.set(this@MainActivity, "repeat", "0".toByteArray())
now_playing_details_repeat?.setImageResource(R.drawable.repeat)
now_playing_details_repeat?.setColorFilter(
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat)
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
ContextCompat.getColor(
this,
R.color.controlForeground
)
)
now_playing_details_repeat?.alpha = 0.2f
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.alpha = 0.2f
CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_OFF))
}
@ -576,14 +594,14 @@ class MainActivity : AppCompatActivity() {
1 -> {
Cache.set(this@MainActivity, "repeat", "1".toByteArray())
now_playing_details_repeat?.setImageResource(R.drawable.repeat)
now_playing_details_repeat?.setColorFilter(
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat)
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
ContextCompat.getColor(
this,
R.color.controlForeground
)
)
now_playing_details_repeat?.alpha = 1.0f
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.alpha = 1.0f
CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_ALL))
}
@ -591,14 +609,14 @@ class MainActivity : AppCompatActivity() {
// From repeat one to no repeat
2 -> {
Cache.set(this@MainActivity, "repeat", "2".toByteArray())
now_playing_details_repeat?.setImageResource(R.drawable.repeat_one)
now_playing_details_repeat?.setColorFilter(
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat_one)
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
ContextCompat.getColor(
this,
R.color.controlForeground
)
)
now_playing_details_repeat?.alpha = 1.0f
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.alpha = 1.0f
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.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.SearchAdapter
import audio.funkwhale.ffa.databinding.ActivitySearchBinding
import audio.funkwhale.ffa.fragments.AddToPlaylistDialog
import audio.funkwhale.ffa.fragments.AlbumsFragment
import audio.funkwhale.ffa.fragments.ArtistsFragment
import audio.funkwhale.ffa.repositories.*
import audio.funkwhale.ffa.utils.*
import com.google.android.exoplayer2.offline.Download
import kotlinx.android.synthetic.main.activity_search.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@ -25,25 +24,28 @@ import java.util.*
class SearchActivity : AppCompatActivity() {
private lateinit var adapter: SearchAdapter
lateinit var artistsRepository: ArtistsSearchRepository
lateinit var albumsRepository: AlbumsSearchRepository
lateinit var tracksRepository: TracksSearchRepository
lateinit var favoritesRepository: FavoritesRepository
private lateinit var artistsRepository: ArtistsSearchRepository
private lateinit var albumsRepository: AlbumsSearchRepository
private lateinit var tracksRepository: TracksSearchRepository
private lateinit var favoritesRepository: FavoritesRepository
private lateinit var binding: ActivitySearchBinding
var done = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
binding = ActivitySearchBinding.inflate(layoutInflater)
adapter = SearchAdapter(this, SearchResultClickListener(), FavoriteListener()).also {
results.layoutManager = LinearLayoutManager(this)
results.adapter = it
}
setContentView(binding.root)
search.requestFocus()
adapter =
SearchAdapter(layoutInflater, this, SearchResultClickListener(), FavoriteListener()).also {
binding.results.layoutManager = LinearLayoutManager(this)
binding.results.adapter = it
}
binding.search.requestFocus()
}
override fun onResume() {
@ -53,7 +55,12 @@ class SearchActivity : AppCompatActivity() {
CommandBus.get().collect { command ->
when (command) {
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, "")
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 {
search.clearFocus()
binding.search.clearFocus()
rawQuery?.let {
done = 0
@ -85,35 +93,38 @@ class SearchActivity : AppCompatActivity() {
albumsRepository.query = query.toLowerCase(Locale.ROOT)
tracksRepository.query = query.toLowerCase(Locale.ROOT)
search_spinner.visibility = View.VISIBLE
search_empty.visibility = View.GONE
search_no_results.visibility = View.GONE
binding.searchSpinner.visibility = View.VISIBLE
binding.searchEmpty.visibility = View.GONE
binding.searchNoResults.visibility = View.GONE
adapter.artists.clear()
adapter.albums.clear()
adapter.tracks.clear()
adapter.notifyDataSetChanged()
artistsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { artists, _, _, _ ->
done++
artistsRepository.fetch(Repository.Origin.Network.origin)
.untilNetwork(lifecycleScope) { artists, _, _, _ ->
done++
adapter.artists.addAll(artists)
refresh()
}
adapter.artists.addAll(artists)
refresh()
}
albumsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { albums, _, _, _ ->
done++
albumsRepository.fetch(Repository.Origin.Network.origin)
.untilNetwork(lifecycleScope) { albums, _, _, _ ->
done++
adapter.albums.addAll(albums)
refresh()
}
adapter.albums.addAll(albums)
refresh()
}
tracksRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { tracks, _, _, _ ->
done++
tracksRepository.fetch(Repository.Origin.Network.origin)
.untilNetwork(lifecycleScope) { tracks, _, _, _ ->
done++
adapter.tracks.addAll(tracks)
refresh()
}
adapter.tracks.addAll(tracks)
refresh()
}
}
return true
@ -127,25 +138,31 @@ class SearchActivity : AppCompatActivity() {
adapter.notifyDataSetChanged()
if (adapter.artists.size + adapter.albums.size + adapter.tracks.size == 0) {
search_no_results.visibility = View.VISIBLE
binding.searchNoResults.visibility = View.VISIBLE
} else {
search_no_results.visibility = View.GONE
binding.searchNoResults.visibility = View.GONE
}
if (done == 3) {
search_spinner.visibility = View.INVISIBLE
binding.searchSpinner.visibility = View.INVISIBLE
}
}
private suspend fun refreshDownloadedTrack(download: Download) {
if (download.state == Download.STATE_COMPLETED) {
download.getMetadata()?.let { info ->
adapter.tracks.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
withContext(Dispatchers.Main) {
adapter.tracks[match.second].downloaded = true
adapter.notifyItemChanged(adapter.getPositionOf(SearchAdapter.ResultType.Track, match.second))
adapter.tracks.withIndex().associate { it.value to it.index }
.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
withContext(Dispatchers.Main) {
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
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.widget.Toast
import androidx.appcompat.app.AlertDialog
@ -10,18 +14,22 @@ import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SeekBarPreference
import audio.funkwhale.ffa.BuildConfig
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.ActivitySettingsBinding
import audio.funkwhale.ffa.utils.Cache
import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus
class SettingsActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
supportFragmentManager
.beginTransaction()
@ -35,7 +43,10 @@ class SettingsActivity : AppCompatActivity() {
fun getThemeResId(): Int = R.style.AppTheme
}
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
class SettingsFragment :
PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onResume() {
super.onResume()
@ -68,7 +79,11 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
Cache.get(activity, "crashdump")?.readLines()?.joinToString("\n").also {
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 {
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.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.Settings
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -5,30 +5,36 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.fragments.OtterAdapter
import audio.funkwhale.ffa.databinding.RowAlbumBinding
import audio.funkwhale.ffa.fragments.FFAAdapter
import audio.funkwhale.ffa.utils.Album
import audio.funkwhale.ffa.utils.maybeLoad
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import com.squareup.picasso.Picasso
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 {
fun onClick(view: View?, album: Album)
}
private lateinit var binding: RowAlbumBinding
override fun getItemId(position: Int): Long = data[position].id.toLong()
override fun getItemCount() = data.size
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 {
view.setOnClickListener(it)
binding = RowAlbumBinding.inflate(layoutInflater, parent, false)
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.artist.text = album.artist.name
holder.release_date.visibility = View.GONE
holder.releaseDate.visibility = View.GONE
album.release_date?.split('-')?.getOrNull(0)?.let { year ->
if (year.isNotEmpty()) {
holder.release_date.visibility = View.VISIBLE
holder.release_date.text = year
holder.releaseDate.visibility = View.VISIBLE
holder.releaseDate.text = year
}
}
}
inner class ViewHolder(view: View, private val listener: OnAlbumClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener {
val art = view.art
val title = view.title
val artist = view.artist
val release_date = view.release_date
inner class ViewHolder(binding: RowAlbumBinding, private val listener: OnAlbumClickListener) :
RecyclerView.ViewHolder(binding.root), View.OnClickListener {
val art = binding.art
val title = binding.title
val artist = binding.artist
val releaseDate = binding.releaseDate
override fun onClick(view: View?) {
listener.onClick(view, data[layoutPosition])

View File

@ -1,20 +1,25 @@
package audio.funkwhale.ffa.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
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.maybeLoad
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import com.squareup.picasso.Picasso
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 {
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 onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_album_grid, parent, false)
return ViewHolder(view, listener).also {
view.setOnClickListener(it)
binding = RowAlbumGridBinding.inflate(layoutInflater, parent, false)
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
}
inner class ViewHolder(view: View, private val listener: OnAlbumClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener {
val cover = view.cover
val title = view.title
inner class ViewHolder(binding: RowAlbumGridBinding, private val listener: OnAlbumClickListener) :
RecyclerView.ViewHolder(binding.root), View.OnClickListener {
val cover = binding.cover
val title = binding.title
override fun onClick(view: View?) {
listener.onClick(view, data[layoutPosition])

View File

@ -6,15 +6,22 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
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.maybeLoad
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import com.squareup.picasso.Picasso
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()
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 onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_artist, parent, false)
return ViewHolder(view, listener).also {
view.setOnClickListener(it)
binding = RowArtistBinding.inflate(layoutInflater, parent, false)
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 {
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 {
val art = view.art
val name = view.name
val albums = view.albums
inner class ViewHolder(binding: RowArtistBinding, private val listener: OnArtistClickListener) :
RecyclerView.ViewHolder(binding.root),
View.OnClickListener {
val art = binding.art
val name = binding.name
val albums = binding.albums
override fun onClick(view: View?) {
listener.onClick(view, active[layoutPosition])

View File

@ -7,17 +7,25 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.RowDownloadBinding
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.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 {
fun onItemRemoved(index: Int)
}
private lateinit var binding: RowDownloadBinding
var downloads: MutableList<DownloadInfo> = mutableListOf()
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 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) {
@ -67,7 +76,12 @@ class DownloadsAdapter(private val context: Context, private val listener: OnDow
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))
}
@ -76,7 +90,13 @@ class DownloadsAdapter(private val context: Context, private val listener: OnDow
holder.toggle.setOnClickListener {
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 -> {
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 {
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) {
val title = view.title
val artist = view.artist
val progress = view.progress
val toggle = view.toggle
val delete = view.delete
inner class ViewHolder(binding: RowDownloadBinding) : RecyclerView.ViewHolder(binding.root) {
val title = binding.title
val artist = binding.artist
val progress = binding.progress
val toggle = binding.toggle
val delete = binding.delete
}
}

View File

@ -11,18 +11,31 @@ import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.fragments.OtterAdapter
import audio.funkwhale.ffa.utils.*
import audio.funkwhale.ffa.databinding.RowTrackBinding
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 jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_track.view.*
import java.util.*
import java.util.Collections
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 {
fun onToggleFavorite(id: Int, state: Boolean)
}
private lateinit var binding: RowTrackBinding
var currentTrack: Track? = null
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 {
val view = LayoutInflater.from(context).inflate(R.layout.row_track, parent, false)
return ViewHolder(view, context).also {
view.setOnClickListener(it)
binding = RowTrackBinding.inflate(layoutInflater, parent, false)
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) {
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) {
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))
}
inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view), View.OnClickListener {
val cover = view.cover
val title = view.title
val artist = view.artist
inner class ViewHolder(binding: RowTrackBinding, val context: Context?) :
RecyclerView.ViewHolder(binding.root),
View.OnClickListener {
val cover = binding.cover
val title = binding.title
val artist = binding.artist
val favorite = view.favorite
val actions = view.actions
val favorite = binding.favorite
val actions = binding.actions
override fun onClick(view: View?) {
when (fromQueue) {

View File

@ -4,24 +4,42 @@ import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
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.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.fragments.OtterAdapter
import audio.funkwhale.ffa.utils.*
import audio.funkwhale.ffa.databinding.RowTrackBinding
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 jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_track.view.*
import java.util.*
import java.util.Collections
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 {
fun onToggleFavorite(id: Int, state: Boolean)
}
private lateinit var binding: RowTrackBinding
interface OnPlaylistListener {
fun onMoveTrack(from: Int, to: 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 {
val view = LayoutInflater.from(context).inflate(R.layout.row_track, parent, false)
return ViewHolder(view, context).also {
view.setOnClickListener(it)
binding = RowTrackBinding.inflate(layoutInflater, parent, false)
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_play_next -> CommandBus.send(Command.PlayNext(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
@ -141,14 +163,16 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
notifyItemMoved(oldPosition, newPosition)
}
inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view), View.OnClickListener {
val handle = view.handle
val cover = view.cover
val title = view.title
val artist = view.artist
inner class ViewHolder(binding: RowTrackBinding, val context: Context?) :
RecyclerView.ViewHolder(binding.root),
View.OnClickListener {
val handle = binding.handle
val cover = binding.cover
val title = binding.title
val artist = binding.artist
val favorite = view.favorite
val actions = view.actions
val favorite = binding.favorite
val actions = binding.actions
override fun onClick(view: View?) {
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) =
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
to = target.adapterPosition

View File

@ -7,27 +7,34 @@ import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
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.toDurationString
import com.squareup.picasso.Picasso
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 {
fun onClick(holder: View?, playlist: Playlist)
}
private lateinit var binding: RowPlaylistBinding
override fun getItemCount() = data.size
override fun getItemId(position: Int) = data[position].id.toLong()
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 {
view.setOnClickListener(it)
return ViewHolder(binding, listener).also {
binding.root.setOnClickListener(it)
}
}
@ -35,24 +42,29 @@ class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistCl
val playlist = data[position]
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 {
ContextCompat.getDrawable(context, R.drawable.cover).let {
holder.cover_top_left.setImageDrawable(it)
holder.cover_top_right.setImageDrawable(it)
holder.cover_bottom_left.setImageDrawable(it)
holder.cover_bottom_right.setImageDrawable(it)
holder.coverTopLeft.setImageDrawable(it)
holder.covertTopRight.setImageDrawable(it)
holder.coverBottomLeft.setImageDrawable(it)
holder.coverBottomRight.setImageDrawable(it)
}
}
playlist.album_covers.shuffled().take(4).forEachIndexed { index, url ->
val imageView = when (index) {
0 -> holder.cover_top_left
1 -> holder.cover_top_right
2 -> holder.cover_bottom_left
3 -> holder.cover_bottom_right
else -> holder.cover_top_left
0 -> holder.coverTopLeft
1 -> holder.covertTopRight
2 -> holder.coverBottomLeft
3 -> holder.coverBottomRight
else -> holder.coverTopLeft
}
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 {
val name = view.name
val summary = view.summary
inner class ViewHolder(
binding: RowPlaylistBinding,
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 cover_top_right = view.cover_top_right
val cover_bottom_left = view.cover_bottom_left
val cover_bottom_right = view.cover_bottom_right
val coverTopLeft = binding.coverTopLeft
val covertTopRight = binding.coverTopRight
val coverBottomLeft = binding.coverBottomLeft
val coverBottomRight = binding.coverBottomRight
override fun onClick(view: View?) {
listener.onClick(view, data[layoutPosition])

View File

@ -6,25 +6,34 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
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.Event
import audio.funkwhale.ffa.utils.EventBus
import audio.funkwhale.ffa.utils.Radio
import audio.funkwhale.ffa.views.LoadingImageView
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.Dispatchers.Main
import kotlinx.coroutines.flow.collect
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 {
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 {
Header,
InstanceRadio,
@ -33,16 +42,43 @@ class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private va
private val instanceRadios: List<Radio> by lazy {
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(
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(
Radio(0, "actor_content", 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))
Radio(
0,
"actor_content",
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 {
return when (viewType) {
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 {
view.setOnClickListener(it)
RowRadioViewHolder(rowRadioBinding, listener).also {
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) {
when (getItemViewType(position)) {
RowType.Header.ordinal -> {
holder as RowRadioHeaderViewHolder
context?.let {
when (position) {
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 -> {
val radio = getRadioAt(position)
holder as RowRadioViewHolder
holder.art.visibility = View.VISIBLE
holder.name.text = radio.name
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 {
val label = view.label
val art = view.art
val name = view.name
val description = view.description
var native = false
override fun onClick(view: View?) {
listener?.onClick(this, getRadioAt(layoutPosition))
}
inner class RowRadioViewHolder(binding: RowRadioBinding, val listener: OnRadioClickListener) :
ViewHolder(binding.root),
View.OnClickListener {
val art = binding.art
val name = binding.name
val description = binding.description
fun spin() {
context?.let {
@ -151,7 +187,6 @@ class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private va
when (message) {
is Event.RadioStarted -> {
art.colorFilter = originalColorFilter
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.recyclerview.widget.RecyclerView
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 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 {
fun onArtistClick(holder: View?, artist: Artist)
fun onAlbumClick(holder: View?, album: Album)
@ -35,7 +50,10 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
Track
}
val SECTION_COUNT = 3
private lateinit var searchHeaderBinding: RowSearchHeaderBinding
private lateinit var rowTrackBinding: RowTrackBinding
val sectionCount = 3
var artists: MutableList<Artist> = mutableListOf()
var albums: MutableList<Album> = mutableListOf()
@ -43,7 +61,7 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
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 {
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 -> 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
}
}
@ -72,26 +90,35 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = when (viewType) {
ResultType.Header.ordinal -> LayoutInflater.from(context).inflate(R.layout.row_search_header, parent, false)
else -> LayoutInflater.from(context).inflate(R.layout.row_track, parent, false)
}
return ViewHolder(view, context).also {
view.setOnClickListener(it)
return when (viewType) {
ResultType.Header.ordinal -> {
searchHeaderBinding = RowSearchHeaderBinding.inflate(layoutInflater, parent, false)
SearchHeaderViewHolder(searchHeaderBinding, context)
}
else -> {
rowTrackBinding = RowTrackBinding.inflate(layoutInflater, parent, false)
RowTrackViewHolder(rowTrackBinding, context).also {
rowTrackBinding.root.setOnClickListener(it)
}
}
}
}
@SuppressLint("NewApi")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val resultType = getItemViewType(position)
val searchHeaderViewHolder = holder as? SearchHeaderViewHolder
val rowTrackViewHolder = holder as? RowTrackViewHolder
if (resultType == ResultType.Header.ordinal) {
context?.let { context ->
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.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()) {
holder.itemView.visibility = View.GONE
@ -100,9 +127,12 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
}
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.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()) {
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)) {
holder.title.text = context.getString(R.string.tracks)
searchHeaderViewHolder?.title?.text = context.getString(R.string.tracks)
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()) {
holder.itemView.visibility = View.GONE
@ -127,20 +160,20 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
val item = when (resultType) {
ResultType.Artist.ordinal -> {
holder.actions.visibility = View.GONE
holder.favorite.visibility = View.GONE
rowTrackViewHolder?.actions?.visibility = View.GONE
rowTrackViewHolder?.favorite?.visibility = View.GONE
artists[position - 1]
}
ResultType.Album.ordinal -> {
holder.actions.visibility = View.GONE
holder.favorite.visibility = View.GONE
rowTrackViewHolder?.actions?.visibility = View.GONE
rowTrackViewHolder?.favorite?.visibility = View.GONE
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]
}
@ -149,65 +182,98 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
.maybeLoad(maybeNormalizeUrl(item.cover()))
.fit()
.transform(RoundedCornersTransformation(16, 0))
.into(holder.cover)
.into(rowTrackViewHolder?.cover)
holder.title.text = item.title()
holder.artist.text = item.subtitle()
searchHeaderViewHolder?.title?.text = item.title()
rowTrackViewHolder?.artist?.text = item.subtitle()
Build.VERSION_CODES.P.onApi(
{
holder.title.setTypeface(holder.title.typeface, Typeface.DEFAULT.weight)
holder.artist.setTypeface(holder.artist.typeface, Typeface.DEFAULT.weight)
searchHeaderViewHolder?.title?.setTypeface(
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)
holder.artist.typeface = Typeface.create(holder.artist.typeface, Typeface.NORMAL)
searchHeaderViewHolder?.title?.typeface =
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) {
(item as? Track)?.let { track ->
context?.let { context ->
if (track == currentTrack || track.current) {
holder.title.setTypeface(holder.title.typeface, Typeface.BOLD)
holder.artist.setTypeface(holder.artist.typeface, Typeface.BOLD)
searchHeaderViewHolder?.title?.setTypeface(
searchHeaderViewHolder.title.typeface,
Typeface.BOLD
)
rowTrackViewHolder?.artist?.setTypeface(
rowTrackViewHolder.artist.typeface,
Typeface.BOLD
)
}
when (track.favorite) {
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected))
true -> rowTrackViewHolder?.favorite?.setColorFilter(context.getColor(R.color.colorFavorite))
false -> rowTrackViewHolder?.favorite?.setColorFilter(context.getColor(R.color.colorSelected))
}
holder.favorite.setOnClickListener {
rowTrackViewHolder?.favorite?.setOnClickListener {
favoriteListener?.let {
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)
}
}
when (track.cached || track.downloaded) {
true -> holder.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0)
false -> holder.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
true -> searchHeaderViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.downloaded,
0,
0,
0
)
false -> searchHeaderViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(
0,
0,
0,
0
)
}
if (track.cached && !track.downloaded) {
holder.title.compoundDrawables.forEach {
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN)
searchHeaderViewHolder?.title?.compoundDrawables?.forEach {
it?.colorFilter =
PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN)
}
}
if (track.downloaded) {
holder.title.compoundDrawables.forEach {
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN)
searchHeaderViewHolder?.title?.compoundDrawables?.forEach {
it?.colorFilter =
PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN)
}
}
holder.actions.setOnClickListener {
PopupMenu(context, holder.actions, Gravity.START, R.attr.actionOverflowMenuStyle, 0).apply {
rowTrackViewHolder?.actions?.setOnClickListener {
PopupMenu(
context,
rowTrackViewHolder.actions,
Gravity.START,
R.attr.actionOverflowMenuStyle,
0
).apply {
inflate(R.menu.row_track)
setOnMenuItemClickListener {
@ -234,19 +300,23 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
return when (type) {
ResultType.Artist -> position + 1
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
}
}
inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view), View.OnClickListener {
val handle = view.handle
val cover = view.cover
val title = view.title
val artist = view.artist
inner class SearchHeaderViewHolder(val binding: RowSearchHeaderBinding, context: Context?) :
ViewHolder(binding.root, context) {
val title = binding.title
}
val favorite = view.favorite
val actions = view.actions
inner class RowTrackViewHolder(val binding: RowTrackBinding, context: Context?) :
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?) {
when (getItemViewType(layoutPosition)) {
@ -263,7 +333,7 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
}
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 {
CommandBus.send(Command.ReplaceQueue(this))
@ -273,8 +343,11 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
}
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.content.Context
import android.graphics.*
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
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.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.fragments.OtterAdapter
import audio.funkwhale.ffa.utils.*
import audio.funkwhale.ffa.databinding.RowTrackBinding
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 jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_track.view.*
import java.util.*
import java.util.Collections
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 {
fun onToggleFavorite(id: Int, state: Boolean)
}
private lateinit var binding: RowTrackBinding
private lateinit var touchHelper: ItemTouchHelper
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 {
val view = LayoutInflater.from(context).inflate(R.layout.row_track, parent, false)
return ViewHolder(view, context).also {
view.setOnClickListener(it)
binding = RowTrackBinding.inflate(layoutInflater, parent, false)
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) {
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) {
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)
}
inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view), View.OnClickListener {
val handle = view.handle
val cover = view.cover
val title = view.title
val artist = view.artist
inner class ViewHolder(binding: RowTrackBinding, val context: Context?) :
RecyclerView.ViewHolder(binding.root),
View.OnClickListener {
val favorite = view.favorite
val actions = view.actions
val handle = binding.handle
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?) {
when (fromQueue) {
@ -188,10 +212,14 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) =
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
to = target.adapterPosition
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
to = target.absoluteAdapterPosition
onItemMove(viewHolder.adapterPosition, to)
onItemMove(viewHolder.absoluteAdapterPosition, to)
return true
}

View File

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

View File

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

View File

@ -1,7 +1,9 @@
package audio.funkwhale.ffa.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -10,24 +12,41 @@ import androidx.transition.Slide
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.activities.MainActivity
import audio.funkwhale.ffa.adapters.AlbumsGridAdapter
import audio.funkwhale.ffa.databinding.FragmentAlbumsGridBinding
import audio.funkwhale.ffa.repositories.AlbumsRepository
import audio.funkwhale.ffa.utils.Album
import audio.funkwhale.ffa.utils.AppContext
import kotlinx.android.synthetic.main.fragment_albums_grid.*
class AlbumsGridFragment : OtterFragment<Album, AlbumsGridAdapter>() {
override val viewRes = R.layout.fragment_albums_grid
override val recycler: RecyclerView get() = albums
class AlbumsGridFragment : FFAFragment<Album, AlbumsGridAdapter>() {
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 alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = AlbumsGridAdapter(context, OnAlbumClickListener())
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 {
override fun onClick(view: View?, album: Album) {
(context as? MainActivity)?.let { activity ->

View File

@ -2,7 +2,9 @@ package audio.funkwhale.ffa.fragments
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
@ -12,19 +14,50 @@ import androidx.transition.Slide
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.activities.MainActivity
import audio.funkwhale.ffa.adapters.ArtistsAdapter
import audio.funkwhale.ffa.databinding.FragmentArtistsBinding
import audio.funkwhale.ffa.repositories.ArtistsRepository
import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.Artist
import audio.funkwhale.ffa.utils.onViewPager
import kotlinx.android.synthetic.main.fragment_artists.*
class ArtistsFragment : OtterFragment<Artist, ArtistsAdapter>() {
override val viewRes = R.layout.fragment_artists
override val recycler: RecyclerView get() = artists
class ArtistsFragment : FFAFragment<Artist, ArtistsAdapter>() {
private var _binding: FragmentArtistsBinding? = null
private val binding get() = _binding!!
override val recycler: RecyclerView get() = binding.artists
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 {
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 {
fragment?.let { fragment ->
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 {
override fun onClick(holder: View?, artist: Artist) {
openAlbums(context, artist, fragment = this@ArtistsFragment)

View File

@ -5,30 +5,42 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.BrowseTabsAdapter
import kotlinx.android.synthetic.main.fragment_browse.view.*
import audio.funkwhale.ffa.databinding.FragmentBrowseBinding
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?) {
super.onCreate(savedInstanceState)
adapter = BrowseTabsAdapter(this, childFragmentManager)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_browse, container, false).apply {
tabs.setupWithViewPager(pager)
tabs.getTabAt(0)?.select()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentBrowseBinding.inflate(inflater)
return binding.root.apply {
binding.tabs.setupWithViewPager(binding.pager)
binding.tabs.getTabAt(0)?.select()
pager.adapter = adapter
pager.offscreenPageLimit = 3
binding.pager.adapter = adapter
binding.pager.offscreenPageLimit = 3
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
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
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import audio.funkwhale.ffa.repositories.HttpUpstream
import audio.funkwhale.ffa.repositories.Repository
import audio.funkwhale.ffa.utils.*
import com.google.gson.Gson
import kotlinx.android.synthetic.main.fragment_artists.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
@ -21,7 +19,7 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
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()
init {
@ -31,26 +29,22 @@ abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adap
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 {
const val OFFSCREEN_PAGES = 20
}
abstract val viewRes: Int
abstract val recycler: RecyclerView
open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context)
open val alwaysRefresh = true
lateinit var repository: Repository<D, *>
lateinit var adapter: A
lateinit var swiper: SwipeRefreshLayout
private var moreLoading = false
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?) {
super.onViewCreated(view, savedInstanceState)
@ -77,7 +71,8 @@ abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
EventBus.get().collect { event ->
if (event is Event.ListingsChanged) {
withContext(Main) {
swiper?.isRefreshing = true
swiper.isRefreshing = true
fetch(Repository.Origin.Network.origin)
}
}
@ -95,13 +90,13 @@ abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
override fun onResume() {
super.onResume()
swiper?.setOnRefreshListener {
swiper.setOnRefreshListener {
fetch(Repository.Origin.Network.origin)
}
}
fun update() {
swiper?.isRefreshing = true
swiper.isRefreshing = true
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) {
lifecycleScope.launch(Main) {
swiper?.isRefreshing = true
swiper.isRefreshing = true
}
}
moreLoading = true
repository.fetch(upstreams, size).untilNetwork(lifecycleScope, IO) { data, isCache, _, hasMore ->
if (isCache && data.isEmpty()) {
moreLoading = false
return@untilNetwork fetch(Repository.Origin.Network.origin)
}
lifecycleScope.launch(Main) {
if (isCache) {
repository.fetch(upstreams, size)
.untilNetwork(lifecycleScope, IO) { data, isCache, _, hasMore ->
if (isCache && data.isEmpty()) {
moreLoading = false
adapter.data = data.toMutableList()
adapter.notifyDataSetChanged()
return@launch
return@untilNetwork fetch(Repository.Origin.Network.origin)
}
if (first) {
adapter.data.clear()
}
lifecycleScope.launch(Main) {
if (isCache) {
moreLoading = false
onDataFetched(data)
adapter.data = data.toMutableList()
adapter.notifyDataSetChanged()
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) {
return@launch
}
}
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)
if (first) {
adapter.data.clear()
}
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 {
moreLoading = false
}
} else {
moreLoading = false
}
}
}
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
when (upstream.behavior) {
HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) 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
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
when (upstream.behavior) {
HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper?.isRefreshing =
false
HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper.isRefreshing = false
HttpUpstream.Behavior.Single -> if (!hasMore) swiper.isRefreshing = 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 {

View File

@ -1,35 +1,62 @@
package audio.funkwhale.ffa.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.FavoritesAdapter
import audio.funkwhale.ffa.databinding.FragmentFavoritesBinding
import audio.funkwhale.ffa.repositories.FavoritesRepository
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 kotlinx.android.synthetic.main.fragment_favorites.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
override val viewRes = R.layout.fragment_favorites
override val recycler: RecyclerView get() = favorites
class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() {
private var _binding: FragmentFavoritesBinding? = null
private val binding get() = _binding!!
override val recycler: RecyclerView get() = binding.favorites
override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = FavoritesAdapter(context, FavoriteListener())
adapter = FavoritesAdapter(layoutInflater, context, FavoriteListener())
repository = FavoritesRepository(context)
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() {
super.onResume()
@ -44,7 +71,7 @@ class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
refreshDownloadedTracks()
}
play.setOnClickListener {
binding.play.setOnClickListener {
CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
}
}
@ -83,12 +110,13 @@ class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
private suspend fun refreshDownloadedTrack(download: Download) {
if (download.state == Download.STATE_COMPLETED) {
download.getMetadata()?.let { info ->
adapter.data.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
withContext(Main) {
adapter.data[match.second].downloaded = true
adapter.notifyItemChanged(match.second)
adapter.data.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }
.toList().getOrNull(0)?.let { match ->
withContext(Main) {
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.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.TracksAdapter
import audio.funkwhale.ffa.utils.*
import kotlinx.android.synthetic.main.partial_queue.*
import kotlinx.android.synthetic.main.partial_queue.view.*
import audio.funkwhale.ffa.databinding.PartialQueueBinding
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 kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class LandscapeQueueFragment : Fragment() {
private var _binding: PartialQueueBinding? = null
private val binding get() = _binding!!
private var adapter: TracksAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
@ -25,32 +34,42 @@ class LandscapeQueueFragment : Fragment() {
watchEventBus()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.partial_queue, container, false).apply {
adapter = TracksAdapter(context, fromQueue = true).also {
queue.layoutManager = LinearLayoutManager(context)
queue.adapter = it
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): 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() {
super.onResume()
queue?.visibility = View.GONE
placeholder?.visibility = View.VISIBLE
binding.queue.visibility = View.GONE
binding.placeholder.visibility = View.VISIBLE
queue_shuffle.setOnClickListener {
binding.queueShuffle.setOnClickListener {
CommandBus.send(Command.ShuffleQueue)
}
queue_save.setOnClickListener {
binding.queueSave.setOnClickListener {
adapter?.data?.let {
CommandBus.send(Command.AddToPlaylist(it))
}
}
queue_clear.setOnClickListener {
binding.queueClear.setOnClickListener {
CommandBus.send(Command.ClearQueue)
}
@ -65,11 +84,11 @@ class LandscapeQueueFragment : Fragment() {
it.notifyDataSetChanged()
if (it.data.isEmpty()) {
queue?.visibility = View.GONE
placeholder?.visibility = View.VISIBLE
binding.queue.visibility = View.GONE
binding.placeholder.visibility = View.VISIBLE
} else {
queue?.visibility = View.VISIBLE
placeholder?.visibility = View.GONE
binding.queue.visibility = View.VISIBLE
binding.placeholder.visibility = View.GONE
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -11,12 +11,16 @@ import android.widget.TextView
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.FragmentTrackInfoDetailsBinding
import audio.funkwhale.ffa.utils.Track
import audio.funkwhale.ffa.utils.mustNormalizeUrl
import audio.funkwhale.ffa.utils.toDurationString
import kotlinx.android.synthetic.main.fragment_track_info_details.*
class TrackInfoDetailsFragment : DialogFragment() {
private var _binding: FragmentTrackInfoDetailsBinding? = null
private val binding get() = _binding!!
companion object {
fun new(track: Track): TrackInfoDetailsFragment {
return TrackInfoDetailsFragment().apply {
@ -27,7 +31,8 @@ class TrackInfoDetailsFragment : DialogFragment() {
"trackCopyright" to track.copyright,
"trackLicense" to track.license,
"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" },
"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_license, getString("trackLicense")))
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_instance, getString("trackInstance")))
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_track_info_details, container, false)
override fun onCreateView(
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?) {
@ -75,11 +95,17 @@ class TrackInfoDetailsFragment : DialogFragment() {
val valueTextView = TextView(context).apply {
text = value ?: "N/A"
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)
infos.addView(valueTextView)
binding.infos.addView(labelTextView)
binding.infos.addView(valueTextView)
}
}
}

View File

@ -4,13 +4,16 @@ import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.TracksAdapter
import audio.funkwhale.ffa.databinding.FragmentTracksBinding
import audio.funkwhale.ffa.repositories.FavoritedRepository
import audio.funkwhale.ffa.repositories.FavoritesRepository
import audio.funkwhale.ffa.repositories.TracksRepository
@ -32,25 +35,21 @@ import com.google.android.exoplayer2.offline.Download
import com.preference.PowerPreference
import com.squareup.picasso.Picasso
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.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class TracksFragment : OtterFragment<Track, TracksAdapter>() {
override val viewRes = R.layout.fragment_tracks
override val recycler: RecyclerView get() = tracks
class TracksFragment : FFAFragment<Track, TracksAdapter>() {
lateinit var favoritesRepository: FavoritesRepository
lateinit var favoritedRepository: FavoritedRepository
override val recycler: RecyclerView get() = binding.tracks
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 albumArtist = ""
@ -80,7 +79,7 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
albumCover = getString("albumCover") ?: ""
}
adapter = TracksAdapter(context, FavoriteListener())
adapter = TracksAdapter(layoutInflater, context, FavoriteListener())
repository = TracksRepository(context, albumId)
favoritesRepository = FavoritesRepository(context)
favoritedRepository = FavoritedRepository(context)
@ -92,8 +91,8 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
when {
data.all { it.downloaded } -> {
title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0)
title.compoundDrawables.forEach {
binding.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0)
binding.title.compoundDrawables.forEach {
it?.colorFilter =
PorterDuffColorFilter(
requireContext().getColor(R.color.downloaded),
@ -102,8 +101,8 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
}
}
data.all { it.cached } -> {
title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0)
title.compoundDrawables.forEach {
binding.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0)
binding.title.compoundDrawables.forEach {
it?.colorFilter =
PorterDuffColorFilter(
requireContext().getColor(R.color.cached),
@ -112,11 +111,26 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
}
}
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?) {
super.onViewCreated(view, savedInstanceState)
@ -126,10 +140,10 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
.fit()
.centerCrop()
.transform(RoundedCornersTransformation(16, 0))
.into(cover)
.into(binding.cover)
artist.text = albumArtist
title.text = albumTitle
binding.artist.text = albumArtist
binding.title.text = albumTitle
}
override fun onResume() {
@ -146,24 +160,24 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
var coverHeight: Float? = null
scroller.setOnScrollChangeListener { _: View?, _: Int, scrollY: Int, _: Int, _: Int ->
binding.scroller.setOnScrollChangeListener { _: View?, _: Int, scrollY: Int, _: Int, _: Int ->
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 ->
cover.alpha = (height - scrollY.toFloat()) / height
binding.cover.alpha = (height - scrollY.toFloat()) / height
}
}
when (PowerPreference.getDefaultFile().getString("play_order")) {
"in_order" -> play.text = getString(R.string.playback_play)
else -> play.text = getString(R.string.playback_shuffle)
"in_order" -> binding.play.text = getString(R.string.playback_play)
else -> binding.play.text = getString(R.string.playback_shuffle)
}
play.setOnClickListener {
binding.play.setOnClickListener {
when (PowerPreference.getDefaultFile().getString("play_order")) {
"in_order" -> CommandBus.send(Command.ReplaceQueue(adapter.data))
else -> CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
@ -173,8 +187,14 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
}
context?.let { context ->
actions.setOnClickListener {
PopupMenu(context, actions, Gravity.START, R.attr.actionOverflowMenuStyle, 0).apply {
binding.actions.setOnClickListener {
PopupMenu(
context,
binding.actions,
Gravity.START,
R.attr.actionOverflowMenuStyle,
0
).apply {
inflate(R.menu.album)
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.TypedValue
import android.view.GestureDetector
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewTreeObserver
import android.view.animation.DecelerateInterpolator
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.PartialNowPlayingBinding
import com.google.android.material.card.MaterialCardView
import kotlinx.android.synthetic.main.partial_now_playing.view.*
import kotlin.math.abs
import kotlin.math.min
@ -20,6 +21,9 @@ class NowPlayingView : MaterialCardView {
var gestureDetector: GestureDetector? = null
var gestureDetectorCallback: OnGestureDetection? = null
private val binding =
PartialNowPlayingBinding.inflate(LayoutInflater.from(context), this, true)
constructor(context: Context) : super(context) {
activity = context
}
@ -35,7 +39,10 @@ class NowPlayingView : MaterialCardView {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
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) {
@ -55,7 +62,7 @@ class NowPlayingView : MaterialCardView {
gestureDetectorCallback?.onUp()
}
}
performClick()
ret
}
@ -93,7 +100,7 @@ class NowPlayingView : MaterialCardView {
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 {
@ -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
layoutParams.let {
@ -138,7 +150,12 @@ class NowPlayingView : MaterialCardView {
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
layoutParams.let {
@ -146,10 +163,10 @@ class NowPlayingView : MaterialCardView {
val progress = (newHeight - minHeight) / (maxHeight - minHeight)
val newMargin = maxMargin - (maxMargin * progress)
(layoutParams as? MarginLayoutParams)?.let {
it.marginStart = newMargin.toInt()
it.marginEnd = newMargin.toInt()
it.bottomMargin = newMargin.toInt()
(layoutParams as? MarginLayoutParams)?.let { params ->
params.marginStart = newMargin.toInt()
params.marginEnd = newMargin.toInt()
params.bottomMargin = newMargin.toInt()
}
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()
}
}
@ -223,9 +240,9 @@ class NowPlayingView : MaterialCardView {
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()
}
}

View File

@ -27,7 +27,9 @@
tools:alpha="1"
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>

View File

@ -1,56 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android: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
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
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">
<TextView
android:id="@+id/title"
style="@style/AppTheme.ItemTitle"
<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_height="wrap_content"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:lines="1"
tools:text="Absolution" />
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/artist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
tools:text="Muse" />
android:id="@+id/title"
style="@style/AppTheme.ItemTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:lines="1"
tools:text="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>
<TextView
android:id="@+id/release_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="0"
android:background="@drawable/pill" />
android:id="@+id/release_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="0"
android:background="@drawable/pill" />
</LinearLayout>