Create dedicated search tab
Move all searching to a tab of its own.
This commit is contained in:
parent
e029785181
commit
8f5e6621ad
27 changed files with 395 additions and 546 deletions
|
@ -64,7 +64,7 @@ dependencies {
|
|||
// General
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.activity:activity-ktx:1.2.0-rc01'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.0-rc01'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01' // Outdated to fix "no event down from INITIALIZED" error
|
||||
|
||||
// Layout
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
|
|
|
@ -150,7 +150,7 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
* Scroll to the currently playing item.
|
||||
*/
|
||||
private fun scrollToPlayingItem() {
|
||||
// Calculate where the item for the currently played song is, -1 if it isnt here
|
||||
// Calculate where the item for the currently played song is
|
||||
val pos = detailModel.albumSortMode.value!!.getSortedSongList(
|
||||
detailModel.currentAlbum.value!!.songs
|
||||
).indexOf(playbackModel.song.value)
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
package org.oxycblt.auxio.library.adapters
|
||||
package org.oxycblt.auxio.library
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
|
@ -13,8 +12,7 @@ import org.oxycblt.auxio.recycler.viewholders.ArtistViewHolder
|
|||
import org.oxycblt.auxio.recycler.viewholders.GenreViewHolder
|
||||
|
||||
/**
|
||||
* A near-identical adapter as [SearchAdapter] but this one isn't a [ListAdapter]
|
||||
* Id love to unify these two adapters but that triggers a bug on the android backend, so...
|
||||
* An adapter for displaying library items.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class LibraryAdapter(
|
|
@ -2,29 +2,22 @@ package org.oxycblt.auxio.library
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.forEach
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.transition.Fade
|
||||
import androidx.transition.TransitionManager
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentLibraryBinding
|
||||
import org.oxycblt.auxio.detail.DetailViewModel
|
||||
import org.oxycblt.auxio.library.adapters.LibraryAdapter
|
||||
import org.oxycblt.auxio.library.adapters.SearchAdapter
|
||||
import org.oxycblt.auxio.logD
|
||||
import org.oxycblt.auxio.logE
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.BaseModel
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.ActionMenu
|
||||
import org.oxycblt.auxio.ui.accent
|
||||
|
@ -39,7 +32,7 @@ import org.oxycblt.auxio.ui.toColor
|
|||
* A [Fragment] that shows a custom list of [Genre], [Artist], or [Album] data. Also allows for
|
||||
* search functionality.
|
||||
*/
|
||||
class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||
class LibraryFragment : Fragment() {
|
||||
private val libraryModel: LibraryViewModel by activityViewModels()
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
||||
|
@ -50,70 +43,21 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
): View {
|
||||
val binding = FragmentLibraryBinding.inflate(inflater)
|
||||
|
||||
val libraryAdapter = LibraryAdapter(::onItemSelection, ::showActionsForItem)
|
||||
val searchAdapter = SearchAdapter(::onItemSelection, ::showActionsForItem)
|
||||
val libraryAdapter = LibraryAdapter(::onItemSelection) { data, view ->
|
||||
ActionMenu(requireCompatActivity(), view, data, ActionMenu.FLAG_NONE)
|
||||
}
|
||||
|
||||
val sortAction = binding.libraryToolbar.menu.findItem(R.id.submenu_sorting)
|
||||
val filterAction = binding.libraryToolbar.menu.findItem(R.id.submenu_filtering)
|
||||
val searchView: SearchView
|
||||
|
||||
// --- UI SETUP ---
|
||||
|
||||
binding.libraryToolbar.apply {
|
||||
menu.apply {
|
||||
val searchAction = findItem(R.id.action_search)
|
||||
searchView = searchAction.actionView as SearchView
|
||||
|
||||
searchView.queryHint = getString(R.string.hint_search_library)
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
searchView.setOnQueryTextListener(this@LibraryFragment)
|
||||
|
||||
searchAction.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
binding.libraryRecycler.adapter = searchAdapter
|
||||
|
||||
searchAction.isVisible = false
|
||||
sortAction.isVisible = false
|
||||
filterAction.isVisible = true
|
||||
|
||||
libraryModel.resetQuery()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
binding.libraryRecycler.adapter = libraryAdapter
|
||||
|
||||
searchAction.isVisible = true
|
||||
sortAction.isVisible = true
|
||||
filterAction.isVisible = false
|
||||
|
||||
libraryModel.resetQuery()
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.action_search -> {
|
||||
TransitionManager.beginDelayedTransition(
|
||||
binding.libraryToolbar, Fade()
|
||||
)
|
||||
it.expandActionView()
|
||||
}
|
||||
|
||||
R.id.submenu_sorting -> {}
|
||||
|
||||
R.id.submenu_filtering -> {}
|
||||
|
||||
else -> {
|
||||
if (sortAction.isVisible) {
|
||||
libraryModel.updateSortMode(it.itemId)
|
||||
} else if (filterAction.isVisible) {
|
||||
libraryModel.updateFilterMode(it.itemId)
|
||||
libraryModel.doSearch(searchView.query.toString(), requireContext())
|
||||
}
|
||||
libraryModel.updateSortMode(it.itemId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,17 +70,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
setHasFixedSize(true)
|
||||
|
||||
if (isLandscape(resources)) {
|
||||
val spans = getLandscapeSpans(resources)
|
||||
|
||||
layoutManager = GridLayoutManager(requireContext(), spans).apply {
|
||||
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return if (binding.libraryRecycler.adapter == searchAdapter) {
|
||||
if (searchAdapter.currentList[position] is Header) spans else 1
|
||||
} else 1
|
||||
}
|
||||
}
|
||||
}
|
||||
layoutManager = GridLayoutManager(requireContext(), getLandscapeSpans(resources))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,14 +80,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
libraryAdapter.updateData(it)
|
||||
}
|
||||
|
||||
libraryModel.searchResults.observe(viewLifecycleOwner) {
|
||||
if (binding.libraryRecycler.adapter == searchAdapter) {
|
||||
searchAdapter.submitList(it) {
|
||||
binding.libraryRecycler.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
libraryModel.sortMode.observe(viewLifecycleOwner) { mode ->
|
||||
logD("Updating sort mode to $mode")
|
||||
|
||||
|
@ -170,20 +96,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
}
|
||||
}
|
||||
|
||||
libraryModel.filterMode.observe(viewLifecycleOwner) { mode ->
|
||||
logD("Updating filter mode to $mode")
|
||||
|
||||
val modeId = mode.toMenuId()
|
||||
|
||||
filterAction.subMenu.forEach {
|
||||
if (it.itemId == modeId) {
|
||||
it.applyColor(accent.first.toColor(requireContext()))
|
||||
} else {
|
||||
it.applyColor(resolveAttr(requireContext(), android.R.attr.textColorPrimary))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
libraryModel.updateNavigationStatus(false)
|
||||
|
@ -207,31 +119,8 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
libraryModel.updateNavigationStatus(false)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
requireView().rootView.clearFocus()
|
||||
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onQueryTextSubmit(query: String): Boolean = false
|
||||
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
libraryModel.doSearch(query, requireContext())
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the [ActionMenu] actions for an item.
|
||||
* @param data The model that the actions should correspond to
|
||||
* @param view The anchor view the menu should be bound to.
|
||||
*/
|
||||
private fun showActionsForItem(data: BaseModel, view: View) {
|
||||
ActionMenu(requireCompatActivity(), view, data, ActionMenu.FLAG_NONE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to an item, or play it, depending on what the given item is.
|
||||
* Navigate to an item
|
||||
* @param baseModel The data things should be done with
|
||||
*/
|
||||
private fun onItemSelection(baseModel: BaseModel) {
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
package org.oxycblt.auxio.library
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.BaseModel
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.recycler.DisplayMode
|
||||
import org.oxycblt.auxio.recycler.SortMode
|
||||
|
@ -27,12 +23,6 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
|||
private val mLibraryData = MutableLiveData(listOf<BaseModel>())
|
||||
val libraryData: LiveData<List<BaseModel>> get() = mLibraryData
|
||||
|
||||
private val mFilterMode = MutableLiveData(DisplayMode.SHOW_ALL)
|
||||
val filterMode: LiveData<DisplayMode> get() = mFilterMode
|
||||
|
||||
private val mSearchResults = MutableLiveData(listOf<BaseModel>())
|
||||
val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
|
||||
|
||||
private var mIsNavigating = false
|
||||
val isNavigating: Boolean get() = mIsNavigating
|
||||
|
||||
|
@ -47,143 +37,10 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
|||
// Set up the display/sort modes
|
||||
mDisplayMode = settingsManager.libraryDisplayMode
|
||||
mSortMode.value = settingsManager.librarySortMode
|
||||
mFilterMode.value = settingsManager.libraryFilterMode
|
||||
|
||||
updateLibraryData()
|
||||
}
|
||||
|
||||
// --- SEARCH FUNCTIONS ---
|
||||
|
||||
/**
|
||||
* Perform a search of the music library, given a query.
|
||||
* Results are pushed to [searchResults].
|
||||
* @param query The query for this search
|
||||
* @param context The context needed to create the header text
|
||||
*/
|
||||
fun doSearch(query: String, context: Context) {
|
||||
// Don't bother if the query is blank.
|
||||
if (query == "") {
|
||||
resetQuery()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val combined = mutableListOf<BaseModel>()
|
||||
|
||||
// Searching is done in a different order depending on which items are being shown
|
||||
// E.G If albums are being shown, then they will be the first items on the list.
|
||||
when (mDisplayMode) {
|
||||
DisplayMode.SHOW_GENRES -> {
|
||||
searchForGenres(combined, query, context)
|
||||
searchForArtists(combined, query, context)
|
||||
searchForAlbums(combined, query, context)
|
||||
}
|
||||
|
||||
DisplayMode.SHOW_ARTISTS -> {
|
||||
searchForArtists(combined, query, context)
|
||||
searchForAlbums(combined, query, context)
|
||||
searchForGenres(combined, query, context)
|
||||
}
|
||||
|
||||
DisplayMode.SHOW_ALBUMS -> {
|
||||
searchForAlbums(combined, query, context)
|
||||
searchForArtists(combined, query, context)
|
||||
searchForGenres(combined, query, context)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
mSearchResults.value = combined
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchForGenres(
|
||||
data: MutableList<BaseModel>,
|
||||
query: String,
|
||||
context: Context
|
||||
): MutableList<BaseModel> {
|
||||
if (mFilterMode.value == DisplayMode.SHOW_ALL ||
|
||||
mFilterMode.value == DisplayMode.SHOW_GENRES
|
||||
) {
|
||||
val genres = musicStore.genres.filter { it.name.contains(query, true) }
|
||||
|
||||
if (genres.isNotEmpty()) {
|
||||
data.add(Header(id = 0, name = context.getString(R.string.label_genres)))
|
||||
data.addAll(genres)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
private fun searchForArtists(
|
||||
data: MutableList<BaseModel>,
|
||||
query: String,
|
||||
context: Context
|
||||
): MutableList<BaseModel> {
|
||||
if (mFilterMode.value == DisplayMode.SHOW_ALL ||
|
||||
mFilterMode.value == DisplayMode.SHOW_ARTISTS
|
||||
) {
|
||||
val artists = musicStore.artists.filter { it.name.contains(query, true) }
|
||||
|
||||
if (artists.isNotEmpty()) {
|
||||
data.add(Header(id = 1, name = context.getString(R.string.label_artists)))
|
||||
data.addAll(artists)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
private fun searchForAlbums(
|
||||
data: MutableList<BaseModel>,
|
||||
query: String,
|
||||
context: Context
|
||||
): MutableList<BaseModel> {
|
||||
if (mFilterMode.value == DisplayMode.SHOW_ALL ||
|
||||
mFilterMode.value == DisplayMode.SHOW_ALBUMS
|
||||
) {
|
||||
val albums = musicStore.albums.filter { it.name.contains(query, true) }
|
||||
|
||||
if (albums.isNotEmpty()) {
|
||||
data.add(Header(id = 2, name = context.getString(R.string.label_albums)))
|
||||
data.addAll(albums)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current filtering mode.
|
||||
*/
|
||||
fun updateFilterMode(@IdRes itemId: Int) {
|
||||
val mode = when (itemId) {
|
||||
R.id.option_filter_all -> DisplayMode.SHOW_ALL
|
||||
R.id.option_filter_albums -> DisplayMode.SHOW_ALBUMS
|
||||
R.id.option_filter_artists -> DisplayMode.SHOW_ARTISTS
|
||||
R.id.option_filter_genres -> DisplayMode.SHOW_GENRES
|
||||
|
||||
else -> DisplayMode.SHOW_ALL
|
||||
}
|
||||
|
||||
if (mFilterMode.value != mode) {
|
||||
mFilterMode.value = mode
|
||||
settingsManager.libraryFilterMode = mode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the query.
|
||||
*/
|
||||
fun resetQuery() {
|
||||
mSearchResults.value = listOf()
|
||||
}
|
||||
|
||||
// --- LIBRARY FUNCTIONS ---
|
||||
|
||||
/**
|
||||
* Update the current [SortMode] with a menu id.
|
||||
* @param itemId The id of the menu item selected.
|
||||
|
|
|
@ -32,16 +32,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
// If the music was already loaded, then don't do it again.
|
||||
if (MusicStore.getInstance().loaded) {
|
||||
findNavController().navigate(
|
||||
LoadingFragmentDirections.actionToMain()
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
): View {
|
||||
val binding = FragmentLoadingBinding.inflate(inflater)
|
||||
|
||||
// Set up the permission launcher, as its disallowed outside of onCreate.
|
||||
|
@ -118,6 +109,15 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
// If the music was already loaded, then don't do it again.
|
||||
if (MusicStore.getInstance().loaded) {
|
||||
findNavController().navigate(
|
||||
LoadingFragmentDirections.actionToMain()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for two things:
|
||||
// - If Auxio needs to show the rationale for getting the READ_EXTERNAL_STORAGE permission.
|
||||
// - If Auxio straight up doesn't have the READ_EXTERNAL_STORAGE permission.
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.oxycblt.auxio.recycler
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.IdRes
|
||||
import org.oxycblt.auxio.R
|
||||
|
||||
/**
|
||||
|
@ -12,20 +11,8 @@ enum class DisplayMode(@DrawableRes val iconRes: Int) {
|
|||
SHOW_ALL(R.drawable.ic_sort_none),
|
||||
SHOW_GENRES(R.drawable.ic_genre),
|
||||
SHOW_ARTISTS(R.drawable.ic_artist),
|
||||
SHOW_ALBUMS(R.drawable.ic_album);
|
||||
|
||||
/**
|
||||
* Get a menu action for this show mode. Corresponds to filter actions.
|
||||
*/
|
||||
@IdRes
|
||||
fun toMenuId(): Int {
|
||||
return when (this) {
|
||||
SHOW_ALL -> (R.id.option_filter_all)
|
||||
SHOW_ALBUMS -> (R.id.option_filter_albums)
|
||||
SHOW_ARTISTS -> (R.id.option_filter_artists)
|
||||
SHOW_GENRES -> (R.id.option_filter_genres)
|
||||
}
|
||||
}
|
||||
SHOW_ALBUMS(R.drawable.ic_album),
|
||||
SHOW_SONGS(R.drawable.ic_song);
|
||||
|
||||
companion object {
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package org.oxycblt.auxio.library.adapters
|
||||
package org.oxycblt.auxio.search
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
146
app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt
Normal file
146
app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt
Normal file
|
@ -0,0 +1,146 @@
|
|||
package org.oxycblt.auxio.search
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.toColor
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
||||
import org.oxycblt.auxio.logD
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.BaseModel
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.ui.ActionMenu
|
||||
import org.oxycblt.auxio.ui.accent
|
||||
import org.oxycblt.auxio.ui.getLandscapeSpans
|
||||
import org.oxycblt.auxio.ui.isLandscape
|
||||
import org.oxycblt.auxio.ui.requireCompatActivity
|
||||
import org.oxycblt.auxio.ui.toColor
|
||||
|
||||
class SearchFragment : Fragment() {
|
||||
// SearchViewModel only scoped to this Fragment
|
||||
private val searchModel: SearchViewModel by viewModels()
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = FragmentSearchBinding.inflate(inflater)
|
||||
|
||||
// Apply the accents manually. Not going through the mess of converting my app's
|
||||
// styling to Material given all the second-and-third-order effects it has.
|
||||
val accent = accent.first.toColor(requireContext())
|
||||
|
||||
val searchAdapter = SearchAdapter(::onItemSelection) { data, view ->
|
||||
ActionMenu(requireCompatActivity(), view, data, ActionMenu.FLAG_NONE)
|
||||
}
|
||||
|
||||
// --- UI SETUP --
|
||||
|
||||
binding.searchTextLayout.apply {
|
||||
boxStrokeColor = accent
|
||||
hintTextColor = ColorStateList.valueOf(accent)
|
||||
setEndIconTintList(
|
||||
ColorStateList.valueOf(R.color.control_color.toColor(requireContext()))
|
||||
)
|
||||
}
|
||||
|
||||
binding.searchEditText.addTextChangedListener {
|
||||
searchModel.doSearch(it?.toString() ?: "", requireContext())
|
||||
}
|
||||
|
||||
binding.searchRecycler.apply {
|
||||
adapter = searchAdapter
|
||||
|
||||
if (isLandscape(resources)) {
|
||||
val spans = getLandscapeSpans(resources)
|
||||
|
||||
layoutManager = GridLayoutManager(requireContext(), spans).apply {
|
||||
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int =
|
||||
if (searchAdapter.currentList[position] is Header) spans else 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
searchModel.searchResults.observe(viewLifecycleOwner) {
|
||||
searchAdapter.submitList(it) {
|
||||
binding.searchRecycler.scrollToPosition(0)
|
||||
}
|
||||
|
||||
if (it.isEmpty()) {
|
||||
binding.searchAppbar.setExpanded(true)
|
||||
binding.searchRecycler.visibility = View.GONE
|
||||
} else {
|
||||
binding.searchRecycler.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
requireView().rootView.clearFocus()
|
||||
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
searchModel.updateNavigationStatus(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to an item, or play it, depending on what the given item is.
|
||||
* @param baseModel The data things should be done with
|
||||
*/
|
||||
private fun onItemSelection(baseModel: BaseModel) {
|
||||
if (baseModel is Song) {
|
||||
val settingsManager = SettingsManager.getInstance()
|
||||
playbackModel.playSong(baseModel, settingsManager.songPlaybackMode)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
requireView().rootView.clearFocus()
|
||||
|
||||
if (!searchModel.isNavigating) {
|
||||
searchModel.updateNavigationStatus(true)
|
||||
|
||||
logD("Navigating to the detail fragment for ${baseModel.name}")
|
||||
|
||||
findNavController().navigate(
|
||||
when (baseModel) {
|
||||
is Genre -> SearchFragmentDirections.actionShowGenre(baseModel.id)
|
||||
is Artist -> SearchFragmentDirections.actionShowArtist(baseModel.id)
|
||||
is Album -> SearchFragmentDirections.actionShowAlbum(baseModel.id, false)
|
||||
|
||||
// If given model wasn't valid, then reset the navigation status
|
||||
// and abort the navigation.
|
||||
else -> {
|
||||
searchModel.updateNavigationStatus(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package org.oxycblt.auxio.search
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.BaseModel
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
|
||||
class SearchViewModel : ViewModel() {
|
||||
private val mSearchResults = MutableLiveData(listOf<BaseModel>())
|
||||
val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
|
||||
|
||||
private var mIsNavigating = false
|
||||
val isNavigating: Boolean get() = mIsNavigating
|
||||
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
|
||||
fun doSearch(query: String, context: Context) {
|
||||
if (query.isEmpty()) {
|
||||
mSearchResults.value = listOf()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val results = mutableListOf<BaseModel>()
|
||||
|
||||
musicStore.artists.filterByOrNull(query)?.let {
|
||||
results.add(Header(id = -1, name = context.getString(R.string.label_artists)))
|
||||
results.addAll(it)
|
||||
}
|
||||
|
||||
musicStore.albums.filterByOrNull(query)?.let {
|
||||
results.add(Header(id = -2, name = context.getString(R.string.label_albums)))
|
||||
results.addAll(it)
|
||||
}
|
||||
|
||||
musicStore.genres.filterByOrNull(query)?.let {
|
||||
results.add(Header(id = -3, name = context.getString(R.string.label_genres)))
|
||||
results.addAll(it)
|
||||
}
|
||||
|
||||
musicStore.songs.filterByOrNull(query)?.let {
|
||||
results.add(Header(id = -4, name = context.getString(R.string.label_songs)))
|
||||
results.addAll(it)
|
||||
}
|
||||
|
||||
mSearchResults.value = results
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<BaseModel>.filterByOrNull(value: String): List<BaseModel>? {
|
||||
val filtered = filter { it.name.contains(value, ignoreCase = true) }
|
||||
|
||||
return if (filtered.isNotEmpty()) filtered else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current navigation status
|
||||
* @param value Whether LibraryFragment is navigating or not
|
||||
*/
|
||||
fun updateNavigationStatus(value: Boolean) {
|
||||
mIsNavigating = value
|
||||
}
|
||||
}
|
|
@ -144,22 +144,6 @@ class SettingsManager private constructor(context: Context) :
|
|||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* The current [DisplayMode] of the library search filtering
|
||||
*/
|
||||
var libraryFilterMode: DisplayMode
|
||||
get() = DisplayMode.valueOfOrFallback(
|
||||
sharedPrefs.getString(
|
||||
Keys.KEY_LIBRARY_FILTER_MODE,
|
||||
DisplayMode.SHOW_ARTISTS.toString()
|
||||
)
|
||||
)
|
||||
set(value) {
|
||||
sharedPrefs.edit()
|
||||
.putString(Keys.KEY_LIBRARY_FILTER_MODE, value.toString())
|
||||
.apply()
|
||||
}
|
||||
|
||||
// --- CALLBACKS ---
|
||||
|
||||
private val callbacks = mutableListOf<Callback>()
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
package org.oxycblt.auxio.songs
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.music.BaseModel
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.recycler.DiffCallback
|
||||
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
|
||||
import org.oxycblt.auxio.recycler.viewholders.SongViewHolder
|
||||
|
||||
class SongSearchAdapter(
|
||||
private val doOnClick: (data: Song) -> Unit,
|
||||
private val doOnLongClick: (data: Song, view: View) -> Unit
|
||||
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback()) {
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (getItem(position)) {
|
||||
is Header -> HeaderViewHolder.ITEM_TYPE
|
||||
is Song -> SongViewHolder.ITEM_TYPE
|
||||
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
|
||||
SongViewHolder.ITEM_TYPE -> SongViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||
|
||||
else -> error("Invalid viewholder item type $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (val item = getItem(position)) {
|
||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||
is Song -> (holder as SongViewHolder).bind(item)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,16 +5,12 @@ import android.os.Build
|
|||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
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.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.transition.Fade
|
||||
import androidx.transition.TransitionManager
|
||||
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
|
||||
import com.reddit.indicatorfastscroll.FastScrollerView
|
||||
import org.oxycblt.auxio.R
|
||||
|
@ -37,9 +33,8 @@ import kotlin.math.ceil
|
|||
* them.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class SongsFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||
class SongsFragment : Fragment() {
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val songsModel: SongsViewModel by activityViewModels()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
// Lazy init the text size so that it doesn't have to be calculated every time.
|
||||
|
@ -59,18 +54,12 @@ class SongsFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
|
||||
val musicStore = MusicStore.getInstance()
|
||||
val songAdapter = SongsAdapter(musicStore.songs, ::playSong, ::showSongMenu)
|
||||
val searchAdapter = SongSearchAdapter(::playSong, ::showSongMenu)
|
||||
|
||||
// --- UI SETUP ---
|
||||
|
||||
binding.songToolbar.apply {
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.action_search -> {
|
||||
TransitionManager.beginDelayedTransition(this, Fade())
|
||||
it.expandActionView()
|
||||
}
|
||||
|
||||
R.id.action_shuffle -> {
|
||||
playbackModel.shuffleAll()
|
||||
}
|
||||
|
@ -78,45 +67,6 @@ class SongsFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
|
||||
true
|
||||
}
|
||||
|
||||
menu.apply {
|
||||
val searchAction = findItem(R.id.action_search)
|
||||
val shuffleAction = findItem(R.id.action_shuffle)
|
||||
val searchView = searchAction.actionView as SearchView
|
||||
|
||||
searchView.queryHint = getString(R.string.hint_search_songs)
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
searchView.setOnQueryTextListener(this@SongsFragment)
|
||||
|
||||
searchAction.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
binding.songRecycler.adapter = searchAdapter
|
||||
searchAction.isVisible = false
|
||||
shuffleAction.isVisible = false
|
||||
|
||||
binding.songFastScroll.visibility = View.INVISIBLE
|
||||
binding.songFastScroll.isActivated = false
|
||||
binding.songFastScrollThumb.visibility = View.INVISIBLE
|
||||
|
||||
songsModel.resetQuery()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
songsModel.resetQuery()
|
||||
|
||||
binding.songRecycler.adapter = songAdapter
|
||||
searchAction.isVisible = true
|
||||
shuffleAction.isVisible = true
|
||||
|
||||
binding.songFastScroll.visibility = View.VISIBLE
|
||||
binding.songFastScrollThumb.visibility = View.VISIBLE
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
binding.songRecycler.apply {
|
||||
|
@ -124,16 +74,7 @@ class SongsFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
setHasFixedSize(true)
|
||||
|
||||
if (isLandscape(resources)) {
|
||||
val spans = getLandscapeSpans(resources)
|
||||
|
||||
layoutManager = GridLayoutManager(requireContext(), spans).apply {
|
||||
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return if (binding.songRecycler.adapter == searchAdapter && position == 0)
|
||||
2 else 1
|
||||
}
|
||||
}
|
||||
}
|
||||
layoutManager = GridLayoutManager(requireContext(), getLandscapeSpans(resources))
|
||||
}
|
||||
|
||||
post {
|
||||
|
@ -146,16 +87,6 @@ class SongsFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
|
||||
setupFastScroller(binding)
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
songsModel.searchResults.observe(viewLifecycleOwner) {
|
||||
if (binding.songRecycler.adapter == searchAdapter) {
|
||||
searchAdapter.submitList(it) {
|
||||
binding.songRecycler.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logD("Fragment created.")
|
||||
|
||||
return binding.root
|
||||
|
@ -167,14 +98,6 @@ class SongsFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
songsModel.doSearch(newText, requireContext())
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||
|
||||
/**
|
||||
* Go through the fast scroller setup process.
|
||||
* @param binding Binding required
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
package org.oxycblt.auxio.songs
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.BaseModel
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
|
||||
class SongsViewModel : ViewModel() {
|
||||
private val mSearchResults = MutableLiveData(listOf<BaseModel>())
|
||||
val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
|
||||
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
|
||||
// --- SEARCH FUNCTIONS ---
|
||||
|
||||
/**
|
||||
* Perform a search of the music library, given a query.
|
||||
* Results are pushed to [searchResults].
|
||||
* @param query The query for this search
|
||||
* @param context The context needed to create the header text
|
||||
*/
|
||||
fun doSearch(query: String, context: Context) {
|
||||
// Don't bother if the query is blank.
|
||||
if (query == "") {
|
||||
resetQuery()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val songs = mutableListOf<BaseModel>().also { list ->
|
||||
list.addAll(
|
||||
musicStore.songs.filter {
|
||||
it.name.contains(query, true)
|
||||
}.toMutableList()
|
||||
)
|
||||
}
|
||||
|
||||
if (songs.isNotEmpty()) {
|
||||
songs.add(0, Header(id = 0, name = context.getString(R.string.label_songs)))
|
||||
}
|
||||
|
||||
mSearchResults.value = songs
|
||||
}
|
||||
}
|
||||
|
||||
fun resetQuery() {
|
||||
mSearchResults.value = listOf()
|
||||
}
|
||||
}
|
11
app/src/main/res/drawable/ic_close.xml
Normal file
11
app/src/main/res/drawable/ic_close.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||
</vector>
|
|
@ -2,10 +2,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/>
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z" />
|
||||
</vector>
|
|
@ -46,9 +46,9 @@
|
|||
android:id="@+id/about_desc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:paddingStart="@dimen/padding_small"
|
||||
android:paddingEnd="@dimen/padding_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:text="@string/app_desc"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
|
|
62
app/src/main/res/layout/fragment_search.xml
Normal file
62
app/src/main/res/layout/fragment_search.xml
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?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">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/search_appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/background"
|
||||
android:elevation="@dimen/elevation_normal">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/search_toolbar"
|
||||
style="@style/Toolbar.Style"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
app:layout_scrollFlags="scroll|enterAlways"
|
||||
app:menu="@menu/menu_search"
|
||||
app:title="@string/label_search" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/search_text_layout"
|
||||
style="@style/Theme.MaterialComponents.DayNight"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:boxBackgroundMode="filled"
|
||||
app:boxStrokeWidth="0dp"
|
||||
app:endIconContentDescription="@string/description_search_clear"
|
||||
app:endIconDrawable="@drawable/ic_close"
|
||||
app:endIconMode="clear_text"
|
||||
app:errorEnabled="false">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/search_edit_text"
|
||||
style="@style/Widget.AppCompat.EditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:hint="@string/hint_search_library"
|
||||
android:imeOptions="actionSearch|flagNoExtractUi"
|
||||
android:inputType="textFilter"
|
||||
android:padding="@dimen/padding_medium"
|
||||
android:textCursorDrawable="@drawable/ui_cursor" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/search_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||
tools:listitem="@layout/item_song" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/action_play"
|
||||
android:title="@string/label_play" />
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/action_play_albums"
|
||||
android:title="@string/label_play" />
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/action_play"
|
||||
android:icon="@drawable/ic_play"
|
||||
|
|
|
@ -1,16 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:icon="@drawable/ic_search"
|
||||
android:title="@string/label_search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="collapseActionView|always"
|
||||
tools:ignore="AlwaysShowAction" />
|
||||
|
||||
<!--
|
||||
This action has to be always shown since otherwise android mangles this action when I make '
|
||||
it invisible and then visible again.
|
||||
|
@ -18,8 +9,8 @@
|
|||
-->
|
||||
<item
|
||||
android:id="@+id/submenu_sorting"
|
||||
android:title="@string/label_sort"
|
||||
android:icon="@drawable/ic_sort_none"
|
||||
android:title="@string/label_sort"
|
||||
app:showAsAction="always">
|
||||
<menu>
|
||||
<group android:id="@+id/group_sorting">
|
||||
|
@ -38,32 +29,4 @@
|
|||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/submenu_filtering"
|
||||
android:title="@string/label_filter"
|
||||
android:icon="@drawable/ic_filter"
|
||||
android:visible="false"
|
||||
app:showAsAction="always">
|
||||
<menu>
|
||||
<group android:id="@+id/group_filtering">
|
||||
<item
|
||||
android:id="@+id/option_filter_all"
|
||||
android:title="@string/label_filter_all"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/option_filter_albums"
|
||||
android:title="@string/label_albums"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/option_filter_artists"
|
||||
android:title="@string/label_artists"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/option_filter_genres"
|
||||
android:title="@string/label_genres"
|
||||
app:showAsAction="never" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
</menu>
|
|
@ -8,6 +8,10 @@
|
|||
android:id="@+id/songs_fragment"
|
||||
android:icon="@drawable/ic_song"
|
||||
android:title="@string/label_songs" />
|
||||
<item
|
||||
android:id="@+id/search_fragment"
|
||||
android:icon="@drawable/ic_search"
|
||||
android:title="@string/label_search" />
|
||||
<item
|
||||
android:id="@+id/settings_fragment"
|
||||
android:icon="@drawable/ic_settings"
|
||||
|
|
36
app/src/main/res/menu/menu_search.xml
Normal file
36
app/src/main/res/menu/menu_search.xml
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/submenu_filtering"
|
||||
android:icon="@drawable/ic_filter"
|
||||
android:title="@string/label_filter"
|
||||
app:showAsAction="ifRoom">
|
||||
<menu>
|
||||
<group
|
||||
android:id="@+id/group_filtering"
|
||||
android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/option_filter_all"
|
||||
android:title="@string/label_filter_all"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/option_filter_songs"
|
||||
android:title="@string/label_songs"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/option_filter_albums"
|
||||
android:title="@string/label_albums"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/option_filter_artists"
|
||||
android:title="@string/label_artists"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/option_filter_genres"
|
||||
android:title="@string/label_genres"
|
||||
app:showAsAction="never" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
</menu>
|
|
@ -1,15 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:icon="@drawable/ic_search"
|
||||
android:title="@string/label_search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="collapseActionView|always"
|
||||
tools:ignore="AlwaysShowAction" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_shuffle"
|
||||
android:icon="@drawable/ic_shuffle"
|
||||
|
|
|
@ -100,4 +100,31 @@
|
|||
android:name="org.oxycblt.auxio.settings.SettingsFragment"
|
||||
android:label="SettingsFragment"
|
||||
tools:layout="@layout/fragment_settings" />
|
||||
<fragment
|
||||
android:id="@+id/search_fragment"
|
||||
android:name="org.oxycblt.auxio.search.SearchFragment"
|
||||
android:label="SearchFragment"
|
||||
tools:layout="@layout/fragment_search">
|
||||
<action
|
||||
android:id="@+id/action_show_genre"
|
||||
app:destination="@id/genre_detail_fragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_show_artist"
|
||||
app:destination="@id/artist_detail_fragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_show_album"
|
||||
app:destination="@id/album_detail_fragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
</fragment>
|
||||
</navigation>
|
|
@ -136,6 +136,7 @@
|
|||
<string name="description_shuffle_off">Turn shuffle off</string>
|
||||
<string name="description_change_loop">Change Repeat Mode</string>
|
||||
<string name="description_auxio_icon">Auxio icon</string>
|
||||
<string name="description_search_clear">Clear search query</string>
|
||||
|
||||
<!-- Placeholder Namespace | Placeholder values -->
|
||||
<string name="placeholder_genre">Unknown Genre</string>
|
||||
|
|
Loading…
Reference in a new issue