kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale-android
Keep the player always on top
rodzic
bdbe14278e
commit
c10b3d4a75
|
@ -1,5 +1,5 @@
|
|||
*.iml
|
||||
**.gradle
|
||||
**/.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) ||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
/>
|
|
@ -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"
|
||||
/>
|
|
@ -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"
|
||||
/>
|
|
@ -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"
|
||||
/>
|
|
@ -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"
|
||||
/>
|
|
@ -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"
|
||||
/>
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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<Boolean>" />
|
||||
<variable name="isLoadingData" type="LiveData<Boolean>" />
|
||||
<variable name="hasResults" type="LiveData<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>
|
|
@ -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>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="transitionDuration">300</integer>
|
||||
</resources>
|
|
@ -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>
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Make the mini player overlay stay on top (contributed by @christophehenry)
|
Ładowanie…
Reference in New Issue