Keep the player always on top

This commit is contained in:
Ryan Harg 2023-01-10 12:56:20 +00:00
parent bdbe14278e
commit c10b3d4a75
37 changed files with 857 additions and 783 deletions

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
*.iml
**.gradle
**/.gradle
/local.properties
/.idea
.DS_Store

View File

@ -5,12 +5,15 @@ import java.util.Properties
plugins {
id("com.android.application")
id("kotlin-android")
id("androidx.navigation.safeargs.kotlin")
id("kotlin-parcelize")
id("org.jlleitschuh.gradle.ktlint") version "11.0.0"
id("com.gladed.androidgitversion") version "0.4.14"
id("com.github.triplet.play") version "3.7.0"
id("de.mobilej.unmock")
id("com.github.ben-manes.versions")
id("org.jetbrains.kotlin.android")
jacoco
}
@ -48,6 +51,7 @@ android {
buildFeatures {
viewBinding = true
dataBinding = true
}
packagingOptions {
@ -158,6 +162,9 @@ play {
}
dependencies {
val navVersion: String by rootProject.extra
val lifecycleVersion: String by rootProject.extra
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0")
@ -166,7 +173,8 @@ dependencies {
implementation("androidx.appcompat:appcompat:1.4.2")
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
implementation("androidx.preference:preference-ktx:1.2.0")
implementation("androidx.recyclerview:recyclerview:1.2.1")
@ -189,7 +197,7 @@ dependencies {
isTransitive = false
}
implementation("com.aliassadi:power-preference-lib:2.0.0")
implementation("com.github.AliAsadi:PowerPreference:2.1.0")
implementation("com.github.kittinunf.fuel:fuel:2.3.1")
implementation("com.github.kittinunf.fuel:fuel-coroutines:2.3.1")
implementation("com.github.kittinunf.fuel:fuel-android:2.3.1")
@ -199,6 +207,10 @@ dependencies {
implementation("jp.wasabeef:picasso-transformations:2.4.0")
implementation("net.openid:appauth:0.11.1")
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
implementation("androidx.navigation:navigation-dynamic-features-fragment:$navVersion")
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.13.3")
testImplementation("androidx.test:core:1.5.0")
@ -206,6 +218,7 @@ dependencies {
testImplementation("org.robolectric:robolectric:4.9.2")
androidTestImplementation("io.mockk:mockk-android:1.13.3")
androidTestImplementation("androidx.navigation:navigation-testing:$navVersion")
}
project.afterEvaluate {

View File

@ -44,10 +44,6 @@
android:name=".activities.MainActivity"
android:screenOrientation="portrait" />
<activity
android:name=".activities.SearchActivity"
android:launchMode="singleTop" />
<activity
android:name=".activities.DownloadsActivity"
android:screenOrientation="portrait" />

View File

@ -16,23 +16,23 @@ import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.SeekBar
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.ActivityMainBinding
import audio.funkwhale.ffa.fragments.AddToPlaylistDialog
import audio.funkwhale.ffa.fragments.AlbumsFragment
import audio.funkwhale.ffa.fragments.ArtistsFragment
import audio.funkwhale.ffa.fragments.BrowseFragment
import audio.funkwhale.ffa.fragments.BrowseFragmentDirections
import audio.funkwhale.ffa.fragments.LandscapeQueueFragment
import audio.funkwhale.ffa.fragments.QueueFragment
import audio.funkwhale.ffa.fragments.TrackInfoDetailsFragment
@ -89,40 +89,47 @@ class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val oAuth: OAuth by inject(OAuth::class.java)
private val navigation: NavController by lazy {
val navHost = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navHost.navController
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AppContext.init(this)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.appbar)
onBackPressedDispatcher.addCallback(this) {
if (binding.nowPlaying.isOpened()) {
binding.nowPlaying.close()
} else {
navigation.navigateUp()
}
}
when (intent.action) {
MediaControlsManager.NOTIFICATION_ACTION_OPEN_QUEUE.toString() -> launchDialog(QueueFragment())
}
supportFragmentManager
.beginTransaction()
.replace(R.id.container, BrowseFragment())
.commit()
watchEventBus()
}
override fun onResume() {
super.onResume()
(binding.container as? DisableableFrameLayout)?.setShouldRegisterTouch { _ ->
if (binding.nowPlaying.isOpened()) {
binding.nowPlaying.close()
return@setShouldRegisterTouch false
findViewById<DisableableFrameLayout?>(R.id.container)?.apply {
setShouldRegisterTouch {
if (binding.nowPlaying.isOpened()) {
binding.nowPlaying.close()
false
} else {
true
}
}
true
}
favoritedRepository.update(this, lifecycleScope)
@ -178,15 +185,6 @@ class MainActivity : AppCompatActivity() {
}
}
override fun onBackPressed() {
if (binding.nowPlaying.isOpened()) {
binding.nowPlaying.close()
return
}
super.onBackPressed()
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
this.menu = menu
@ -226,18 +224,11 @@ class MainActivity : AppCompatActivity() {
when (item.itemId) {
android.R.id.home -> {
binding.nowPlaying.close()
(supportFragmentManager.fragments.last() as? BrowseFragment)?.let {
it.selectTabAt(0)
return true
}
launchFragment(BrowseFragment())
navigation.popBackStack(R.id.browseFragment, false)
}
R.id.nav_queue -> launchDialog(QueueFragment())
R.id.nav_search -> startActivity(Intent(this, SearchActivity::class.java))
R.id.nav_search -> navigation.navigate(BrowseFragmentDirections.browseToSearch())
R.id.nav_all_music, R.id.nav_my_music, R.id.nav_followed -> {
menu?.let { menu ->
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW)
@ -300,26 +291,8 @@ class MainActivity : AppCompatActivity() {
return true
}
private fun launchFragment(fragment: Fragment) {
supportFragmentManager.fragments.lastOrNull()?.also { oldFragment ->
oldFragment.enterTransition = null
oldFragment.exitTransition = null
supportFragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
supportFragmentManager
.beginTransaction()
.setCustomAnimations(0, 0, 0, 0)
.replace(R.id.container, fragment)
.commit()
}
private fun launchDialog(fragment: DialogFragment) {
supportFragmentManager.beginTransaction().let {
fragment.show(it, "")
}
}
private fun launchDialog(fragment: DialogFragment) =
fragment.show(supportFragmentManager.beginTransaction(), "")
@SuppressLint("NewApi")
private fun watchEventBus() {
@ -343,7 +316,7 @@ class MainActivity : AppCompatActivity() {
}
} else if (event is Event.PlaybackStopped) {
if (binding.nowPlaying.visibility == View.VISIBLE) {
(binding.container.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
(binding.navHostFragment.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
it.bottomMargin = it.bottomMargin / 2
}
@ -368,15 +341,17 @@ class MainActivity : AppCompatActivity() {
} else if (event is Event.StateChanged) {
when (event.playing) {
true -> {
binding.nowPlayingContainer?.nowPlayingToggle?.icon = getDrawable(R.drawable.pause)
binding.nowPlayingContainer?.nowPlayingToggle?.icon =
AppCompatResources.getDrawable(this@MainActivity, R.drawable.pause)
binding.nowPlayingContainer?.nowPlayingDetailsToggle?.icon =
getDrawable(R.drawable.pause)
AppCompatResources.getDrawable(this@MainActivity, R.drawable.pause)
}
false -> {
binding.nowPlayingContainer?.nowPlayingToggle?.icon = getDrawable(R.drawable.play)
binding.nowPlayingContainer?.nowPlayingToggle?.icon =
AppCompatResources.getDrawable(this@MainActivity, R.drawable.play)
binding.nowPlayingContainer?.nowPlayingDetailsToggle?.icon =
getDrawable(R.drawable.play)
AppCompatResources.getDrawable(this@MainActivity, R.drawable.play)
}
}
} else if (event is Event.QueueChanged) {
@ -459,7 +434,7 @@ class MainActivity : AppCompatActivity() {
.setListener(null)
.start()
(binding.container.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
(binding.navHostFragment?.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
it.bottomMargin = it.bottomMargin * 2
}
@ -534,12 +509,11 @@ class MainActivity : AppCompatActivity() {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.track_info_artist -> ArtistsFragment.openAlbums(
this@MainActivity,
R.id.track_info_artist -> BrowseFragmentDirections.browseToAlbums(
track.artist,
art = track.album?.cover()
track.album?.cover()
)
R.id.track_info_album -> AlbumsFragment.openTracks(this@MainActivity, track.album)
R.id.track_info_album -> track.album?.let(BrowseFragmentDirections::browseToTracks)
R.id.track_info_details -> TrackInfoDetailsFragment.new(track)
.show(supportFragmentManager, "dialog")
}

View File

@ -1,195 +0,0 @@
package audio.funkwhale.ffa.activities
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import audio.funkwhale.ffa.adapters.FavoriteListener
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.model.Album
import audio.funkwhale.ffa.model.Artist
import audio.funkwhale.ffa.repositories.AlbumsSearchRepository
import audio.funkwhale.ffa.repositories.ArtistsSearchRepository
import audio.funkwhale.ffa.repositories.FavoritesRepository
import audio.funkwhale.ffa.repositories.Repository
import audio.funkwhale.ffa.repositories.TracksSearchRepository
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.getMetadata
import audio.funkwhale.ffa.utils.untilNetwork
import com.google.android.exoplayer2.offline.Download
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URLEncoder
import java.util.Locale
class SearchActivity : AppCompatActivity() {
private lateinit var adapter: SearchAdapter
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)
artistsRepository = ArtistsSearchRepository(this@SearchActivity, "")
albumsRepository = AlbumsSearchRepository(this@SearchActivity, "")
tracksRepository = TracksSearchRepository(this@SearchActivity, "")
favoritesRepository = FavoritesRepository(this@SearchActivity)
binding = ActivitySearchBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.search.requestFocus()
}
override fun onResume() {
super.onResume()
lifecycleScope.launch(Dispatchers.Main) {
CommandBus.get().collect { command ->
if (command is Command.AddToPlaylist) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
AddToPlaylistDialog.show(
layoutInflater,
this@SearchActivity,
lifecycleScope,
command.tracks
)
}
}
}
}
lifecycleScope.launch(Dispatchers.IO) {
EventBus.get().collect { event ->
if (event is Event.DownloadChanged) refreshDownloadedTrack(event.download)
}
}
adapter =
SearchAdapter(
layoutInflater,
this,
SearchResultClickListener(),
FavoriteListener(favoritesRepository)
).also {
binding.results.layoutManager = LinearLayoutManager(this)
binding.results.adapter = it
}
binding.search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(rawQuery: String?): Boolean {
binding.search.clearFocus()
rawQuery?.let {
done = 0
val query = URLEncoder.encode(it, "UTF-8")
artistsRepository.query = query.lowercase(Locale.ROOT)
albumsRepository.query = query.lowercase(Locale.ROOT)
tracksRepository.query = query.lowercase(Locale.ROOT)
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++
adapter.artists.addAll(artists)
refresh()
}
albumsRepository.fetch(Repository.Origin.Network.origin)
.untilNetwork(lifecycleScope) { albums, _, _, _ ->
done++
adapter.albums.addAll(albums)
refresh()
}
tracksRepository.fetch(Repository.Origin.Network.origin)
.untilNetwork(lifecycleScope) { tracks, _, _, _ ->
done++
adapter.tracks.addAll(tracks)
refresh()
}
}
return true
}
override fun onQueryTextChange(newText: String?) = true
})
}
private fun refresh() {
adapter.notifyDataSetChanged()
if (adapter.artists.size + adapter.albums.size + adapter.tracks.size == 0) {
binding.searchNoResults.visibility = View.VISIBLE
} else {
binding.searchNoResults.visibility = View.GONE
}
if (done == 3) {
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
)
)
}
}
}
}
}
inner class SearchResultClickListener : SearchAdapter.OnSearchResultClickListener {
override fun onArtistClick(holder: View?, artist: Artist) {
ArtistsFragment.openAlbums(this@SearchActivity, artist)
}
override fun onAlbumClick(holder: View?, album: Album) {
AlbumsFragment.openTracks(this@SearchActivity, album)
}
}
}

View File

@ -7,10 +7,10 @@ import android.graphics.PorterDuffColorFilter
import android.graphics.Typeface
import android.os.Build
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.RowSearchHeaderBinding
@ -24,11 +24,13 @@ import audio.funkwhale.ffa.utils.CoverArt
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import audio.funkwhale.ffa.utils.onApi
import audio.funkwhale.ffa.utils.toast
import audio.funkwhale.ffa.viewmodel.SearchViewModel
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
class SearchAdapter(
private val layoutInflater: LayoutInflater,
private val context: Context?,
viewModel: SearchViewModel,
private val fragment: Fragment,
private val listener: OnSearchResultClickListener,
private val favoriteListener: FavoriteListener
) : RecyclerView.Adapter<SearchAdapter.ViewHolder>() {
@ -50,12 +52,27 @@ class SearchAdapter(
val sectionCount = 3
var artists: MutableList<Artist> = mutableListOf()
var albums: MutableList<Album> = mutableListOf()
var tracks: MutableList<Track> = mutableListOf()
var artists = listOf<Artist>()
var albums = listOf<Album>()
var tracks = listOf<Track>()
var currentTrack: Track? = null
init {
viewModel.artistResults.observe(fragment.viewLifecycleOwner) {
artists = it
this.notifyDataSetChanged()
}
viewModel.albumResults.observe(fragment.viewLifecycleOwner) {
albums = it
this.notifyDataSetChanged()
}
viewModel.trackResults.observe(fragment.viewLifecycleOwner) {
tracks = it
this.notifyDataSetChanged()
}
}
override fun getItemCount() = sectionCount + artists.size + albums.size + tracks.size
override fun getItemId(position: Int): Long {
@ -67,7 +84,7 @@ class SearchAdapter(
}
ResultType.Artist.ordinal -> artists[position].id.toLong()
ResultType.Artist.ordinal -> albums[position - artists.size - 2].id.toLong()
ResultType.Album.ordinal -> albums[position - artists.size - 2].id.toLong()
ResultType.Track.ordinal ->
tracks[position - artists.size - albums.size - sectionCount].id.toLong()
else -> 0
@ -86,12 +103,12 @@ class SearchAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return when (viewType) {
ResultType.Header.ordinal -> {
searchHeaderBinding = RowSearchHeaderBinding.inflate(layoutInflater, parent, false)
SearchHeaderViewHolder(searchHeaderBinding, context)
searchHeaderBinding = RowSearchHeaderBinding.inflate(fragment.layoutInflater, parent, false)
SearchHeaderViewHolder(searchHeaderBinding, fragment.requireContext())
}
else -> {
rowTrackBinding = RowTrackBinding.inflate(layoutInflater, parent, false)
RowTrackViewHolder(rowTrackBinding, context).also {
rowTrackBinding = RowTrackBinding.inflate(fragment.layoutInflater, parent, false)
RowTrackViewHolder(rowTrackBinding, fragment.requireContext()).also {
rowTrackBinding.root.setOnClickListener(it)
}
}
@ -105,47 +122,45 @@ class SearchAdapter(
val rowTrackViewHolder = holder as? RowTrackViewHolder
if (resultType == ResultType.Header.ordinal) {
context?.let { context ->
if (position == 0) {
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
)
if (position == 0) {
searchHeaderViewHolder?.title?.text = fragment.requireContext().getString(R.string.artists)
holder.itemView.visibility = View.VISIBLE
holder.itemView.layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
if (artists.isEmpty()) {
holder.itemView.visibility = View.GONE
holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
}
if (artists.isEmpty()) {
holder.itemView.visibility = View.GONE
holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
}
}
if (position == (artists.size + 1)) {
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
)
if (position == (artists.size + 1)) {
searchHeaderViewHolder?.title?.text = fragment.requireContext().getString(R.string.albums)
holder.itemView.visibility = View.VISIBLE
holder.itemView.layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
if (albums.isEmpty()) {
holder.itemView.visibility = View.GONE
holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
}
if (albums.isEmpty()) {
holder.itemView.visibility = View.GONE
holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
}
}
if (position == (artists.size + albums.size + 2)) {
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
)
if (position == (artists.size + albums.size + 2)) {
searchHeaderViewHolder?.title?.text = fragment.requireContext().getString(R.string.tracks)
holder.itemView.visibility = View.VISIBLE
holder.itemView.layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
if (tracks.isEmpty()) {
holder.itemView.visibility = View.GONE
holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
}
if (tracks.isEmpty()) {
holder.itemView.visibility = View.GONE
holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
}
}
@ -174,7 +189,7 @@ class SearchAdapter(
else -> tracks[position]
}
CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(item.cover()))
CoverArt.withContext(fragment.layoutInflater.context, maybeNormalizeUrl(item.cover()))
.fit()
.transform(RoundedCornersTransformation(16, 0))
.into(rowTrackViewHolder?.cover)
@ -216,90 +231,91 @@ class SearchAdapter(
}
ResultType.Track.ordinal -> {
(item as? Track)?.let { track ->
context?.let { context ->
if (track == currentTrack || track.current) {
searchHeaderViewHolder?.title?.setTypeface(
searchHeaderViewHolder.title.typeface,
Typeface.BOLD
)
rowTrackViewHolder?.artist?.setTypeface(
rowTrackViewHolder.artist.typeface,
Typeface.BOLD
)
if (track == currentTrack || track.current) {
searchHeaderViewHolder?.title?.setTypeface(
searchHeaderViewHolder.title.typeface,
Typeface.BOLD
)
rowTrackViewHolder?.artist?.setTypeface(
rowTrackViewHolder.artist.typeface,
Typeface.BOLD
)
}
when (track.favorite) {
true -> rowTrackViewHolder?.favorite?.setColorFilter(
fragment.requireContext().getColor(R.color.colorFavorite)
)
false -> rowTrackViewHolder?.favorite?.setColorFilter(
fragment.requireContext().getColor(R.color.colorSelected)
)
}
rowTrackViewHolder?.favorite?.setOnClickListener {
favoriteListener.let {
favoriteListener.onToggleFavorite(track.id, !track.favorite)
tracks[position - artists.size - albums.size - sectionCount].favorite =
!track.favorite
notifyItemChanged(position)
}
}
when (track.favorite) {
true -> rowTrackViewHolder?.favorite?.setColorFilter(
context.getColor(R.color.colorFavorite)
)
false -> rowTrackViewHolder?.favorite?.setColorFilter(
context.getColor(R.color.colorSelected)
)
when (track.cached || track.downloaded) {
true -> rowTrackViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.downloaded, 0, 0, 0
)
false -> rowTrackViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(
0, 0, 0, 0
)
}
if (track.cached && !track.downloaded) {
rowTrackViewHolder?.title?.compoundDrawables?.forEach {
it?.colorFilter =
PorterDuffColorFilter(
fragment.requireContext().getColor(R.color.cached),
PorterDuff.Mode.SRC_IN
)
}
}
rowTrackViewHolder?.favorite?.setOnClickListener {
favoriteListener.let {
favoriteListener.onToggleFavorite(track.id, !track.favorite)
tracks[position - artists.size - albums.size - sectionCount].favorite =
!track.favorite
notifyItemChanged(position)
}
if (track.downloaded) {
rowTrackViewHolder?.title?.compoundDrawables?.forEach {
it?.colorFilter =
PorterDuffColorFilter(
fragment.requireContext().getColor(R.color.downloaded),
PorterDuff.Mode.SRC_IN
)
}
}
when (track.cached || track.downloaded) {
true -> rowTrackViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.downloaded, 0, 0, 0
)
false -> rowTrackViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(
0, 0, 0, 0
)
}
rowTrackViewHolder?.actions?.setOnClickListener {
PopupMenu(
fragment.requireContext(),
rowTrackViewHolder.actions,
Gravity.START,
R.attr.actionOverflowMenuStyle,
0
).apply {
inflate(R.menu.row_track)
if (track.cached && !track.downloaded) {
rowTrackViewHolder?.title?.compoundDrawables?.forEach {
it?.colorFilter =
PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN)
}
}
if (track.downloaded) {
rowTrackViewHolder?.title?.compoundDrawables?.forEach {
it?.colorFilter =
PorterDuffColorFilter(
context.getColor(R.color.downloaded),
PorterDuff.Mode.SRC_IN
setOnMenuItemClickListener {
when (it.itemId) {
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
R.id.track_pin -> CommandBus.send(Command.PinTrack(track))
R.id.track_add_to_playlist -> CommandBus.send(
Command.AddToPlaylist(listOf(track))
)
}
}
rowTrackViewHolder?.actions?.setOnClickListener {
PopupMenu(
context,
rowTrackViewHolder.actions,
Gravity.START,
R.attr.actionOverflowMenuStyle,
0
).apply {
inflate(R.menu.row_track)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
R.id.track_pin -> CommandBus.send(Command.PinTrack(track))
R.id.track_add_to_playlist -> CommandBus.send(
Command.AddToPlaylist(listOf(track))
)
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track))
}
true
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track))
}
show()
true
}
show()
}
}
}
@ -316,12 +332,12 @@ class SearchAdapter(
}
}
inner class SearchHeaderViewHolder(val binding: RowSearchHeaderBinding, context: Context?) :
inner class SearchHeaderViewHolder(val binding: RowSearchHeaderBinding, context: Context) :
ViewHolder(binding.root, context) {
val title = binding.title
}
inner class RowTrackViewHolder(val binding: RowTrackBinding, context: Context?) :
inner class RowTrackViewHolder(val binding: RowTrackBinding, context: Context) :
ViewHolder(binding.root, context), View.OnClickListener {
val title = binding.title
val cover = binding.cover

View File

@ -1,36 +1,27 @@
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
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.transition.Fade
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.model.Album
import audio.funkwhale.ffa.model.Artist
import audio.funkwhale.ffa.repositories.AlbumsRepository
import audio.funkwhale.ffa.repositories.ArtistTracksRepository
import audio.funkwhale.ffa.repositories.Repository
import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.CoverArt
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import audio.funkwhale.ffa.utils.onViewPager
import com.preference.PowerPreference
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.coroutines.Dispatchers.IO
@ -45,77 +36,22 @@ class AlbumsFragment : FFAFragment<Album, AlbumsAdapter>() {
override val recycler: RecyclerView get() = binding.albums
override val alwaysRefresh = false
private val args by navArgs<AlbumsFragmentArgs>()
private val artistArt: String get() = when {
!args.cover.isNullOrBlank() -> args.cover!!
else -> args.artist.cover() ?: ""
}
private var _binding: FragmentAlbumsBinding? = null
private val binding get() = _binding!!
private lateinit var artistTracksRepository: ArtistTracksRepository
private var artistId = 0
private var artistName = ""
private var artistArt = ""
companion object {
fun new(artist: Artist, _art: String? = null): AlbumsFragment {
val art = _art ?: if (artist.albums?.isNotEmpty() == true) artist.cover() else ""
return AlbumsFragment().apply {
arguments = bundleOf(
"artistId" to artist.id,
"artistName" to artist.name,
"artistArt" to art
)
}
}
fun openTracks(context: Context?, album: Album?, fragment: Fragment? = null) {
if (album == null) {
return
}
(context as? MainActivity)?.let {
fragment?.let { fragment ->
fragment.onViewPager {
exitTransition = Fade().apply {
duration = AppContext.TRANSITION_DURATION
interpolator = AccelerateDecelerateInterpolator()
view?.let {
addTarget(it)
}
}
}
}
}
(context as? AppCompatActivity)?.let { activity ->
val nextFragment = TracksFragment.new(album).apply {
enterTransition = Slide().apply {
duration = AppContext.TRANSITION_DURATION
interpolator = AccelerateDecelerateInterpolator()
}
}
activity.supportFragmentManager
.beginTransaction()
.replace(R.id.container, nextFragment)
.addToBackStack(null)
.commit()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.apply {
artistId = getInt("artistId")
artistName = getString("artistName") ?: ""
artistArt = getString("artistArt") ?: ""
}
adapter = AlbumsAdapter(layoutInflater, context, OnAlbumClickListener())
repository = AlbumsRepository(context, artistId)
artistTracksRepository = ArtistTracksRepository(context, artistId)
repository = AlbumsRepository(context, args.artist.id)
artistTracksRepository = ArtistTracksRepository(context, args.artist.id)
}
override fun onCreateView(
@ -151,7 +87,7 @@ class AlbumsFragment : FFAFragment<Album, AlbumsAdapter>() {
.into(cover)
}
binding.artist.text = artistName
binding.artist.text = args.artist.name
}
override fun onResume() {
@ -209,7 +145,7 @@ class AlbumsFragment : FFAFragment<Album, AlbumsAdapter>() {
inner class OnAlbumClickListener : AlbumsAdapter.OnAlbumClickListener {
override fun onClick(view: View?, album: Album) {
openTracks(context, album, fragment = this@AlbumsFragment)
findNavController().navigate(AlbumsFragmentDirections.albumsToTracks(album))
}
}
}

View File

@ -4,18 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.Fade
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.model.Album
import audio.funkwhale.ffa.repositories.AlbumsRepository
import audio.funkwhale.ffa.utils.AppContext
class AlbumsGridFragment : FFAFragment<Album, AlbumsGridAdapter>() {
@ -49,29 +44,7 @@ class AlbumsGridFragment : FFAFragment<Album, AlbumsGridAdapter>() {
inner class OnAlbumClickListener : AlbumsGridAdapter.OnAlbumClickListener {
override fun onClick(view: View?, album: Album) {
(context as? MainActivity)?.let { activity ->
exitTransition = Fade().apply {
duration = AppContext.TRANSITION_DURATION
interpolator = AccelerateDecelerateInterpolator()
view?.let {
addTarget(it)
}
}
val fragment = TracksFragment.new(album).apply {
enterTransition = Slide().apply {
duration = AppContext.TRANSITION_DURATION
interpolator = AccelerateDecelerateInterpolator()
}
}
activity.supportFragmentManager
.beginTransaction()
.replace(R.id.container, fragment)
.addToBackStack(null)
.commit()
}
findNavController().navigate(BrowseFragmentDirections.browseToTracks(album))
}
}
}

View File

@ -1,27 +1,17 @@
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
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.Fade
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.model.Artist
import audio.funkwhale.ffa.repositories.ArtistsRepository
import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.onViewPager
class ArtistsFragment : FFAFragment<Artist, ArtistsAdapter>() {
private var _binding: FragmentArtistsBinding? = null
private val binding get() = _binding!!
@ -50,49 +40,9 @@ class ArtistsFragment : FFAFragment<Artist, ArtistsAdapter>() {
_binding = null
}
companion object {
fun openAlbums(
context: Context?,
artist: Artist,
fragment: Fragment? = null,
art: String? = null
) {
(context as? MainActivity)?.let {
fragment?.let { fragment ->
fragment.onViewPager {
exitTransition = Fade().apply {
duration = AppContext.TRANSITION_DURATION
interpolator = AccelerateDecelerateInterpolator()
view?.let {
addTarget(it)
}
}
}
}
}
(context as? AppCompatActivity)?.let { activity ->
val nextFragment = AlbumsFragment.new(artist, art).apply {
enterTransition = Slide().apply {
duration = AppContext.TRANSITION_DURATION
interpolator = AccelerateDecelerateInterpolator()
}
}
activity.supportFragmentManager
.beginTransaction()
.replace(R.id.container, nextFragment)
.addToBackStack(null)
.commit()
}
}
}
inner class OnArtistClickListener : ArtistsAdapter.OnArtistClickListener {
override fun onClick(holder: View?, artist: Artist) {
openAlbums(context, artist, fragment = this@ArtistsFragment)
findNavController().navigate(BrowseFragmentDirections.browseToAlbums(artist))
}
}
}

View File

@ -14,13 +14,6 @@ class BrowseFragment : Fragment() {
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)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -30,10 +23,11 @@ class BrowseFragment : Fragment() {
return binding.root.apply {
binding.tabs.getTabAt(0)?.select()
val adapter = BrowseTabsAdapter(this@BrowseFragment)
binding.pager.adapter = adapter
binding.pager.offscreenPageLimit = 3
TabLayoutMediator(binding.tabs, binding.pager) { tab, position ->
tab.text = adapter?.tabText(position)
tab.text = adapter.tabText(position)
}.attach()
}
}
@ -42,8 +36,4 @@ class BrowseFragment : Fragment() {
super.onDestroyView()
_binding = null
}
fun selectTabAt(position: Int) {
binding.tabs.getTabAt(position)?.select()
}
}

View File

@ -6,14 +6,13 @@ 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.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.FavoriteListener
import audio.funkwhale.ffa.adapters.PlaylistTracksAdapter
import audio.funkwhale.ffa.databinding.FragmentTracksBinding
import audio.funkwhale.ffa.model.Playlist
import audio.funkwhale.ffa.model.PlaylistTrack
import audio.funkwhale.ffa.model.Track
import audio.funkwhale.ffa.repositories.FavoritesRepository
@ -33,43 +32,19 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
class PlaylistTracksFragment : FFAFragment<PlaylistTrack, PlaylistTracksAdapter>() {
override val recycler: RecyclerView get() = binding.tracks
private val args by navArgs<PlaylistTracksFragmentArgs>()
private var _binding: FragmentTracksBinding? = null
private val binding get() = _binding!!
lateinit var favoritesRepository: FavoritesRepository
lateinit var playlistsRepository: ManagementPlaylistsRepository
var albumId = 0
var albumArtist = ""
var albumTitle = ""
var albumCover = ""
companion object {
fun new(playlist: Playlist): PlaylistTracksFragment {
return PlaylistTracksFragment().apply {
arguments = bundleOf(
"albumId" to playlist.id,
"albumArtist" to "N/A",
"albumTitle" to playlist.name,
"albumCover" to ""
)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.apply {
albumId = getInt("albumId")
albumArtist = getString("albumArtist") ?: ""
albumTitle = getString("albumTitle") ?: ""
albumCover = getString("albumCover") ?: ""
}
favoritesRepository = FavoritesRepository(context)
playlistsRepository = ManagementPlaylistsRepository(context)
@ -79,7 +54,7 @@ class PlaylistTracksFragment : FFAFragment<PlaylistTrack, PlaylistTracksAdapter>
FavoriteListener(favoritesRepository),
PlaylistListener()
)
repository = PlaylistTracksRepository(context, albumId)
repository = PlaylistTracksRepository(context, args.playlist.id)
watchEventBus()
}
@ -105,8 +80,8 @@ class PlaylistTracksFragment : FFAFragment<PlaylistTrack, PlaylistTracksAdapter>
binding.cover.visibility = View.INVISIBLE
binding.covers.visibility = View.VISIBLE
binding.artist.text = "Playlist"
binding.title.text = albumTitle
binding.artist.text = getString(R.string.playlist)
binding.title.text = args.playlist.name
}
override fun onResume() {
@ -216,12 +191,12 @@ class PlaylistTracksFragment : FFAFragment<PlaylistTrack, PlaylistTracksAdapter>
inner class PlaylistListener : PlaylistTracksAdapter.OnPlaylistListener {
override fun onMoveTrack(from: Int, to: Int) {
playlistsRepository.move(albumId, from, to)
playlistsRepository.move(args.playlist.id, from, to)
}
override fun onRemoveTrackFromPlaylist(track: Track, index: Int) {
lifecycleScope.launch(Main) {
playlistsRepository.remove(albumId, index)
playlistsRepository.remove(args.playlist.id, index)
update()
}
}

View File

@ -4,17 +4,12 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.Fade
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.model.Playlist
import audio.funkwhale.ffa.repositories.PlaylistsRepository
import audio.funkwhale.ffa.utils.AppContext
class PlaylistsFragment : FFAFragment<Playlist, PlaylistsAdapter>() {
@ -48,29 +43,7 @@ class PlaylistsFragment : FFAFragment<Playlist, PlaylistsAdapter>() {
inner class OnPlaylistClickListener : PlaylistsAdapter.OnPlaylistClickListener {
override fun onClick(holder: View?, playlist: Playlist) {
(context as? MainActivity)?.let { activity ->
exitTransition = Fade().apply {
duration = AppContext.TRANSITION_DURATION
interpolator = AccelerateDecelerateInterpolator()
view?.let {
addTarget(it)
}
}
val fragment = PlaylistTracksFragment.new(playlist).apply {
enterTransition = Slide().apply {
duration = AppContext.TRANSITION_DURATION
interpolator = AccelerateDecelerateInterpolator()
}
}
activity.supportFragmentManager
.beginTransaction()
.replace(R.id.container, fragment)
.addToBackStack(null)
.commit()
}
findNavController().navigate(BrowseFragmentDirections.browseToPlaylistTracks(playlist))
}
}
}

View File

@ -0,0 +1,136 @@
package audio.funkwhale.ffa.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import audio.funkwhale.ffa.adapters.FavoriteListener
import audio.funkwhale.ffa.adapters.SearchAdapter
import audio.funkwhale.ffa.databinding.FragmentSearchBinding
import audio.funkwhale.ffa.model.Album
import audio.funkwhale.ffa.model.Artist
import audio.funkwhale.ffa.repositories.FavoritesRepository
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.getMetadata
import audio.funkwhale.ffa.viewmodel.SearchViewModel
import com.google.android.exoplayer2.offline.Download
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SearchFragment : Fragment() {
private lateinit var adapter: SearchAdapter
private lateinit var binding: FragmentSearchBinding
private val viewModel by activityViewModels<SearchViewModel>()
private val noSearchYet = MutableLiveData(true)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentSearchBinding.inflate(layoutInflater, container, false)
binding.lifecycleOwner = this
binding.isLoadingData = viewModel.isLoadingData
binding.hasResults = viewModel.hasResults
binding.noSearchYet = noSearchYet
return binding.root
}
override fun onResume() {
super.onResume()
binding.search.requestFocus()
lifecycleScope.launch(Dispatchers.Main) {
CommandBus.get().collect { command ->
if (command is Command.AddToPlaylist) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
AddToPlaylistDialog.show(
layoutInflater,
requireActivity(),
lifecycleScope,
command.tracks
)
}
}
}
}
lifecycleScope.launch(Dispatchers.IO) {
EventBus.get().collect { event ->
if (event is Event.DownloadChanged) refreshDownloadedTrack(event.download)
}
}
adapter =
SearchAdapter(
viewModel,
this,
SearchResultClickListener(),
FavoriteListener(FavoritesRepository(requireContext()))
).also {
binding.results.layoutManager = LinearLayoutManager(requireContext())
binding.results.adapter = it
}
binding.search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
binding.search.clearFocus()
noSearchYet.value = false
viewModel.query.postValue(query)
return true
}
override fun onQueryTextChange(newText: String) = true
})
}
override fun onDestroy() {
super.onDestroy()
// Empty the research to prevent result recall the next time
viewModel.query.value = ""
}
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
)
)
}
}
}
}
}
inner class SearchResultClickListener : SearchAdapter.OnSearchResultClickListener {
override fun onArtistClick(holder: View?, artist: Artist) {
findNavController().navigate(SearchFragmentDirections.searchToAlbums(artist))
}
override fun onAlbumClick(holder: View?, album: Album) {
findNavController().navigate(SearchFragmentDirections.searchToTracks(album))
}
}
}

View File

@ -8,14 +8,13 @@ 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.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.adapters.FavoriteListener
import audio.funkwhale.ffa.adapters.TracksAdapter
import audio.funkwhale.ffa.databinding.FragmentTracksBinding
import audio.funkwhale.ffa.model.Album
import audio.funkwhale.ffa.model.Track
import audio.funkwhale.ffa.repositories.FavoritedRepository
import audio.funkwhale.ffa.repositories.FavoritesRepository
@ -43,7 +42,7 @@ import kotlinx.coroutines.withContext
import org.koin.java.KoinJavaComponent.inject
class TracksFragment : FFAFragment<Track, TracksAdapter>() {
private val args by navArgs<TracksFragmentArgs>()
private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java)
override val recycler: RecyclerView get() = binding.tracks
@ -54,37 +53,12 @@ class TracksFragment : FFAFragment<Track, TracksAdapter>() {
private lateinit var favoritesRepository: FavoritesRepository
private lateinit var favoritedRepository: FavoritedRepository
private var albumId = 0
private var albumArtist = ""
private var albumTitle = ""
private var albumCover = ""
companion object {
fun new(album: Album): TracksFragment {
return TracksFragment().apply {
arguments = bundleOf(
"albumId" to album.id,
"albumArtist" to album.artist.name,
"albumTitle" to album.title,
"albumCover" to album.cover()
)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.apply {
albumId = getInt("albumId")
albumArtist = getString("albumArtist") ?: ""
albumTitle = getString("albumTitle") ?: ""
albumCover = getString("albumCover") ?: ""
}
favoritesRepository = FavoritesRepository(context)
favoritedRepository = FavoritedRepository(context)
repository = TracksRepository(context, albumId)
repository = TracksRepository(context, args.album.id)
adapter = TracksAdapter(layoutInflater, context, FavoriteListener(favoritesRepository))
@ -144,15 +118,15 @@ class TracksFragment : FFAFragment<Track, TracksAdapter>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(albumCover))
CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(args.album.cover()))
.noFade()
.fit()
.centerCrop()
.transform(RoundedCornersTransformation(16, 0))
.into(binding.cover)
binding.artist.text = albumArtist
binding.title.text = albumTitle
binding.artist.text = args.album.artist.name
binding.title.text = args.album.title
}
override fun onResume() {

View File

@ -1,13 +1,18 @@
package audio.funkwhale.ffa.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Album(
val id: Int,
val artist: Artist,
val title: String,
val cover: Covers?,
val release_date: String?
) : SearchResult {
data class Artist(val name: String)
) : SearchResult, Parcelable {
@Parcelize
data class Artist(val name: String) : Parcelable
override fun cover() = cover?.urls?.original
override fun title() = title

View File

@ -1,17 +1,21 @@
package audio.funkwhale.ffa.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.util.Calendar.DAY_OF_YEAR
import java.util.GregorianCalendar
@Parcelize
data class Artist(
val id: Int,
val name: String,
val albums: List<Album>?
) : SearchResult {
) : SearchResult, Parcelable {
@Parcelize
data class Album(
val title: String,
val cover: Covers?
)
) : Parcelable
override fun cover(): String? = albums?.mapNotNull { it.cover?.urls?.original }?.let { covers ->
if (covers.isEmpty()) {
@ -21,6 +25,7 @@ data class Artist(
val index = GregorianCalendar().get(DAY_OF_YEAR) % covers.size
covers.getOrNull(index)
}
override fun title() = name
override fun subtitle() = "Artist"
}

View File

@ -1,3 +1,7 @@
package audio.funkwhale.ffa.model
data class CoverUrls(val original: String)
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class CoverUrls(val original: String) : Parcelable

View File

@ -1,3 +1,7 @@
package audio.funkwhale.ffa.model
data class Covers(val urls: CoverUrls)
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Covers(val urls: CoverUrls) : Parcelable

View File

@ -1,9 +1,13 @@
package audio.funkwhale.ffa.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Playlist(
val id: Int,
val name: String,
val album_covers: List<String>,
val tracks_count: Int,
val duration: Int
)
) : Parcelable

View File

@ -1,8 +1,12 @@
package audio.funkwhale.ffa.model
import android.os.Parcelable
import audio.funkwhale.ffa.utils.containsIgnoringCase
import com.preference.PowerPreference
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class Track(
val id: Int = 0,
val title: String,
@ -13,10 +17,17 @@ data class Track(
val uploads: List<Upload> = listOf(),
val copyright: String? = null,
val license: String? = null
) : SearchResult {
) : SearchResult, Parcelable {
@IgnoredOnParcel
var current: Boolean = false
@IgnoredOnParcel
var favorite: Boolean = false
@IgnoredOnParcel
var cached: Boolean = false
@IgnoredOnParcel
var downloaded: Boolean = false
companion object {
@ -30,7 +41,8 @@ data class Track(
)
}
data class Upload(val listen_url: String, val duration: Int, val bitrate: Int)
@Parcelize
data class Upload(val listen_url: String, val duration: Int, val bitrate: Int) : Parcelable
fun matchesFilter(filter: String): Boolean {
return title.containsIgnoringCase(filter) ||

View File

@ -3,8 +3,8 @@ package audio.funkwhale.ffa.utils
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.fragment.app.Fragment
import audio.funkwhale.ffa.fragments.BrowseFragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import audio.funkwhale.ffa.model.DownloadInfo
import audio.funkwhale.ffa.repositories.Repository
import com.github.kittinunf.fuel.core.FuelError
@ -34,14 +34,6 @@ inline fun <D> Flow<Repository.Response<D>>.untilNetwork(
}
}
fun Fragment.onViewPager(block: Fragment.() -> Unit) {
for (f in activity?.supportFragmentManager?.fragments ?: listOf()) {
if (f is BrowseFragment) {
f.block()
}
}
}
fun <T> Int.onApi(block: () -> T) {
if (Build.VERSION.SDK_INT >= this) {
block()
@ -107,3 +99,53 @@ fun Date.format(): String {
fun String?.containsIgnoringCase(candidate: String): Boolean =
this != null && this.lowercase().contains(candidate.lowercase())
inline fun <T, U, V, R> LiveData<T>.mergeWith(
u: LiveData<U>,
v: LiveData<V>,
crossinline block: (valT: T, valU: U, valV: V) -> R
): LiveData<R> = MediatorLiveData<R>().apply {
addSource(this@mergeWith) {
if (u.value != null && v.value != null) {
postValue(block(it, u.value!!, v.value!!))
}
}
addSource(u) {
if (this@mergeWith.value != null && u.value != null) {
postValue(block(this@mergeWith.value!!, it, v.value!!))
}
}
addSource(v) {
if (this@mergeWith.value != null && u.value != null) {
postValue(block(this@mergeWith.value!!, u.value!!, it))
}
}
}
inline fun <T, U, V, W, R> LiveData<T>.mergeWith(
u: LiveData<U>,
v: LiveData<V>,
w: LiveData<W>,
crossinline block: (valT: T, valU: U, valV: V, valW: W) -> R
): LiveData<R> = MediatorLiveData<R>().apply {
addSource(this@mergeWith) {
if (u.value != null && v.value != null && w.value != null) {
postValue(block(it, u.value!!, v.value!!, w.value!!))
}
}
addSource(u) {
if (this@mergeWith.value != null && v.value != null && w.value != null) {
postValue(block(this@mergeWith.value!!, it, v.value!!, w.value!!))
}
}
addSource(v) {
if (this@mergeWith.value != null && u.value != null && w.value != null) {
postValue(block(this@mergeWith.value!!, u.value!!, it, w.value!!))
}
}
addSource(w) {
if (this@mergeWith.value != null && u.value != null && v.value != null) {
postValue(block(this@mergeWith.value!!, u.value!!, v.value!!, it))
}
}
}

View File

@ -0,0 +1,118 @@
package audio.funkwhale.ffa.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.model.Album
import audio.funkwhale.ffa.model.Artist
import audio.funkwhale.ffa.model.Track
import audio.funkwhale.ffa.repositories.AlbumsSearchRepository
import audio.funkwhale.ffa.repositories.ArtistsSearchRepository
import audio.funkwhale.ffa.repositories.Repository
import audio.funkwhale.ffa.repositories.TracksSearchRepository
import audio.funkwhale.ffa.utils.mergeWith
import audio.funkwhale.ffa.utils.untilNetwork
import kotlinx.coroutines.Dispatchers
import java.net.URLEncoder
import java.util.Locale
class SearchViewModel(app: Application) : AndroidViewModel(app), Observer<String> {
private val artistResultsLoading = MutableLiveData(false)
private val albumResultsLoading = MutableLiveData(false)
private val tackResultsLoading = MutableLiveData(false)
private val artistsRepository =
ArtistsSearchRepository(getApplication<FFA>().applicationContext, "")
private val albumsRepository =
AlbumsSearchRepository(getApplication<FFA>().applicationContext, "")
private val tracksRepository =
TracksSearchRepository(getApplication<FFA>().applicationContext, "")
private val dedupQuery: LiveData<String>
val query = MutableLiveData("")
val artistResults: LiveData<List<Artist>> = MutableLiveData(listOf())
val albumResults: LiveData<List<Album>> = MutableLiveData(listOf())
val trackResults: LiveData<List<Track>> = MutableLiveData(listOf())
val isLoadingData: LiveData<Boolean> = artistResultsLoading.mergeWith(
albumResultsLoading, tackResultsLoading
) { b1, b2, b3 -> b1 || b2 || b3 }
val hasResults: LiveData<Boolean> = isLoadingData.mergeWith(
artistResults, albumResults, trackResults
) { b, r1, r2, r3 -> b || r1.isNotEmpty() || r2.isNotEmpty() || r3.isNotEmpty() }
init {
dedupQuery = query.map { it.trim().lowercase(Locale.ROOT) }.distinctUntilChanged()
dedupQuery.observeForever(this)
}
override fun onChanged(token: String) {
if (token.isBlank()) { // Empty search
(artistResults as MutableLiveData).postValue(listOf())
(albumResults as MutableLiveData).postValue(listOf())
(trackResults as MutableLiveData).postValue(listOf())
return
}
artistResultsLoading.postValue(true)
albumResultsLoading.postValue(true)
tackResultsLoading.postValue(true)
val encoded = URLEncoder.encode(token, "UTF-8")
(artistResults as MutableLiveData).postValue(listOf())
artistsRepository.apply {
query = encoded
fetch(Repository.Origin.Network.origin).untilNetwork(
viewModelScope,
Dispatchers.IO
) { data, _, _, hasMore ->
artistResults.postValue(artistResults.value!! + data)
if (!hasMore) {
artistResultsLoading.postValue(false)
}
}
}
(albumResults as MutableLiveData).postValue(listOf())
albumsRepository.apply {
query = encoded
fetch(Repository.Origin.Network.origin).untilNetwork(
viewModelScope,
Dispatchers.IO
) { data, _, _, hasMore ->
albumResults.postValue(albumResults.value!! + data)
if (!hasMore) {
albumResultsLoading.postValue(false)
}
}
}
(trackResults as MutableLiveData).postValue(listOf())
tracksRepository.apply {
query = encoded
fetch(Repository.Origin.Network.origin).untilNetwork(
viewModelScope,
Dispatchers.IO
) { data, _, _, hasMore ->
trackResults.postValue(trackResults.value!! + data)
if (!hasMore) {
tackResultsLoading.postValue(false)
}
}
}
}
override fun onCleared() {
dedupQuery.removeObserver(this)
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha
xmlns:android="http://schemas.android.com/apk/res/android"
android:startOffset="@integer/transitionDuration"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="@integer/transitionDuration"
/>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:duration="@integer/transitionDuration"
/>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:toAlpha="0.0"
android:fromAlpha="1.0"
android:duration="@integer/transitionDuration"
/>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:fromAlpha="1.0"
android:toAlpha="1.0"
/>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:fromYDelta="0"
android:toYDelta="100%"
android:duration="@integer/transitionDuration"
/>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:fromYDelta="100%"
android:toYDelta="0"
android:duration="@integer/transitionDuration"
/>

View File

@ -11,13 +11,16 @@
android:baselineAligned="false"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize"
android:layout_weight="1"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_marginBottom="?attr/actionBarSize"
app:defaultNavHost="true"
app:navGraph="@navigation/main_nav"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<FrameLayout
android:id="@+id/landscape_queue"

View File

@ -6,11 +6,14 @@
android:layout_height="match_parent"
android:background="@color/surface">
<audio.funkwhale.ffa.views.DisableableFrameLayout
android:id="@+id/container"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize"
app:defaultNavHost="true"
app:navGraph="@navigation/main_nav"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<audio.funkwhale.ffa.views.NowPlayingView

View File

@ -1,100 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:context=".activities.SearchActivity">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="0dp"
android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.SearchView
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:iconifiedByDefault="false"
app:queryBackground="@android:color/transparent"
app:queryHint="@string/search_placeholder" />
<ProgressBar
android:id="@+id/search_spinner"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-12dp"
android:layout_marginBottom="-12dp"
android:indeterminate="true"
android:visibility="invisible" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/search_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:drawablePadding="16dp"
android:text="@string/search_welcome"
android:textAlignment="center"
android:textSize="14sp"
app:drawableTint="#525252"
app:drawableTopCompat="@drawable/funkwhaleshape" />
<TextView
android:id="@+id/search_no_results"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:drawableTopCompat="@drawable/funkwhaleshape"
android:drawablePadding="16dp"
app:drawableTint="#525252"
android:text="@string/search_no_results"
android:textAlignment="center"
android:textSize="14sp"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/results"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:itemCount="10"
tools:listitem="@layout/row_track" />
</LinearLayout>
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="androidx.lifecycle.LiveData" />
<import type="android.view.View" />
<variable name="noSearchYet" type="LiveData&lt;Boolean>" />
<variable name="isLoadingData" type="LiveData&lt;Boolean>" />
<variable name="hasResults" type="LiveData&lt;Boolean>" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/surface">
<LinearLayout
android:id="@+id/search_bar_and_messages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="0dp"
android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.SearchView
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:iconifiedByDefault="false"
app:queryBackground="@android:color/transparent"
app:queryHint="@string/search_placeholder" />
<ProgressBar
android:id="@+id/search_spinner"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-12dp"
android:layout_marginBottom="-12dp"
android:indeterminate="true"
android:visibility="@{isLoadingData ? View.VISIBLE : View.INVISIBLE, default=invisible}" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/search_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/search_welcome"
android:textAlignment="center"
android:textSize="14sp"
android:visibility="@{noSearchYet ? View.VISIBLE : View.GONE, default=visible}"
app:drawableTint="#525252"
app:drawableTopCompat="@drawable/funkwhaleshape" />
<TextView
android:id="@+id/search_no_results"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/search_no_results"
android:textAlignment="center"
android:textSize="14sp"
android:visibility="@{noSearchYet || hasResults ? View.GONE : View.VISIBLE, default=gone}"
app:drawableTint="#525252"
app:drawableTopCompat="@drawable/funkwhaleshape" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/results"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/surface"
app:layout_constraintTop_toBottomOf="@+id/search_bar_and_messages"
app:layout_constraintBottom_toBottomOf="parent"
tools:itemCount="10"
tools:listitem="@layout/row_track" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_nav"
app:startDestination="@id/browseFragment">
<fragment
android:id="@+id/browseFragment"
android:name="audio.funkwhale.ffa.fragments.BrowseFragment"
android:label="BrowseFragment">
<action
android:id="@+id/browseToSearch"
app:destination="@id/searchFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/browseToAlbums"
app:destination="@id/albumsFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/browseToTracks"
app:destination="@id/tracksFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/browseToArtists"
app:destination="@id/artistsFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/browseToPlaylistTracks"
app:destination="@id/playlistTracksFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
</fragment>
<fragment
android:id="@+id/playlistTracksFragment"
android:name="audio.funkwhale.ffa.fragments.PlaylistTracksFragment"
android:label="PlaylistTracksFragment" >
<argument
android:name="playlist"
app:argType="audio.funkwhale.ffa.model.Playlist" />
</fragment>
<fragment
android:id="@+id/tracksFragment"
android:name="audio.funkwhale.ffa.fragments.TracksFragment"
android:label="TracksFragment" >
<argument
android:name="album"
app:argType="audio.funkwhale.ffa.model.Album" />
</fragment>
<fragment
android:id="@+id/albumsFragment"
android:name="audio.funkwhale.ffa.fragments.AlbumsFragment"
android:label="AlbumsFragment" >
<argument
android:name="artist"
app:argType="audio.funkwhale.ffa.model.Artist" />
<argument
android:name="cover"
app:argType="string"
app:nullable="true"
android:defaultValue="@null" />
<action
android:id="@+id/albumsToTracks"
app:destination="@id/tracksFragment" />
</fragment>
<fragment
android:id="@+id/searchFragment"
android:name="audio.funkwhale.ffa.fragments.SearchFragment"
android:label="SearchFragment" >
<action
android:id="@+id/searchToAlbums"
app:destination="@id/albumsFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/searchToTracks"
app:destination="@id/tracksFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
</fragment>
<fragment
android:id="@+id/artistsFragment"
android:name="audio.funkwhale.ffa.fragments.ArtistsFragment"
android:label="ArtistsFragment" />
</navigation>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="transitionDuration">300</integer>
</resources>

View File

@ -127,4 +127,7 @@
<item quantity="one">Downloading %1$d track</item>
<item quantity="other">Downloading %1$d tracks</item>
</plurals>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="playlist">Playlist</string>
</resources>

View File

@ -1,4 +1,8 @@
buildscript {
extra.apply{
set("navVersion", "2.5.2")
set("lifecycleVersion", "2.5.1")
}
repositories {
google()
@ -6,20 +10,22 @@ buildscript {
gradlePluginPortal()
}
val navVersion: String by extra
dependencies {
classpath("com.android.tools.build:gradle:7.3.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
classpath("com.github.bjoernq:unmockplugin:0.7.9")
classpath("com.github.ben-manes:gradle-versions-plugin:0.44.0")
classpath("org.jacoco:org.jacoco.core:0.8.8")
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion")
}
}
allprojects {
repositories {
google()
jcenter()
mavenCentral()
maven(url = "https://jitpack.io")
}
}

View File

@ -0,0 +1 @@
Make the mini player overlay stay on top (contributed by @christophehenry)