Ryan Harg
1 year ago
37 changed files with 857 additions and 783 deletions
@ -1,195 +0,0 @@
@@ -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) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,136 @@
@@ -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)) |
||||
} |
||||
} |
||||
} |
@ -1,3 +1,7 @@
@@ -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 @@
@@ -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 @@
@@ -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 |
||||
|
@ -0,0 +1,118 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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" |
||||
/> |
@ -1,100 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources> |
||||
<integer name="transitionDuration">300</integer> |
||||
</resources> |
Loading…
Reference in new issue