diff --git a/app/build.gradle b/app/build.gradle index 9e811e3eb..4f5aea9c3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 23d1d9860..34ce2a801 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/library/adapters/LibraryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryAdapter.kt similarity index 89% rename from app/src/main/java/org/oxycblt/auxio/library/adapters/LibraryAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/library/LibraryAdapter.kt index 8e2bf679c..fe154d84b 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/adapters/LibraryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryAdapter.kt @@ -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( diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt index d12e69a3a..dc638c079 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -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) { diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt index 31c74868b..531fedb7e 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt @@ -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()) val libraryData: LiveData> get() = mLibraryData - private val mFilterMode = MutableLiveData(DisplayMode.SHOW_ALL) - val filterMode: LiveData get() = mFilterMode - - private val mSearchResults = MutableLiveData(listOf()) - val searchResults: LiveData> 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() - - // 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, - query: String, - context: Context - ): MutableList { - 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, - query: String, - context: Context - ): MutableList { - 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, - query: String, - context: Context - ): MutableList { - 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. diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt index 02ed51e80..5ff509205 100644 --- a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt @@ -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. diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/DisplayMode.kt b/app/src/main/java/org/oxycblt/auxio/recycler/DisplayMode.kt index 126c2f745..e766351bf 100644 --- a/app/src/main/java/org/oxycblt/auxio/recycler/DisplayMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/recycler/DisplayMode.kt @@ -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 { /** diff --git a/app/src/main/java/org/oxycblt/auxio/library/adapters/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/library/adapters/SearchAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt index eea6968c1..ba6171cc2 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/adapters/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -1,4 +1,4 @@ -package org.oxycblt.auxio.library.adapters +package org.oxycblt.auxio.search import android.view.View import android.view.ViewGroup diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt new file mode 100644 index 000000000..7eb7d61e3 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -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 + } + } + ) + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt new file mode 100644 index 000000000..867d6b059 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -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()) + val searchResults: LiveData> 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() + + 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.filterByOrNull(value: String): List? { + 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 + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt index 97a4697df..b8d9fff1b 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt @@ -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() diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongSearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongSearchAdapter.kt deleted file mode 100644 index f186513f0..000000000 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongSearchAdapter.kt +++ /dev/null @@ -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(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) - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt index 90431c55e..c1841f54e 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsViewModel.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsViewModel.kt deleted file mode 100644 index af1e81bd7..000000000 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsViewModel.kt +++ /dev/null @@ -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()) - val searchResults: LiveData> 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().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() - } -} diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 000000000..63ddebde7 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml index 7436eb6aa..52ada64e3 100644 --- a/app/src/main/res/drawable/ic_filter.xml +++ b/app/src/main/res/drawable/ic_filter.xml @@ -2,10 +2,10 @@ - + android:viewportHeight="24"> + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index 44fa44f09..86da6361c 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -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" diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml new file mode 100644 index 000000000..d8ede5db6 --- /dev/null +++ b/app/src/main/res/layout/fragment_search.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_album_actions.xml b/app/src/main/res/menu/menu_album_actions.xml index bdbed3882..6dba7cf26 100644 --- a/app/src/main/res/menu/menu_album_actions.xml +++ b/app/src/main/res/menu/menu_album_actions.xml @@ -1,6 +1,5 @@ - + diff --git a/app/src/main/res/menu/menu_artist_actions.xml b/app/src/main/res/menu/menu_artist_actions.xml index 027814c0f..8bdbcd20b 100644 --- a/app/src/main/res/menu/menu_artist_actions.xml +++ b/app/src/main/res/menu/menu_artist_actions.xml @@ -1,6 +1,5 @@ - + diff --git a/app/src/main/res/menu/menu_genre_actions.xml b/app/src/main/res/menu/menu_genre_actions.xml index bff2d3f90..0d1564ecf 100644 --- a/app/src/main/res/menu/menu_genre_actions.xml +++ b/app/src/main/res/menu/menu_genre_actions.xml @@ -1,6 +1,5 @@ - + - - - @@ -38,32 +29,4 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_nav.xml b/app/src/main/res/menu/menu_nav.xml index d7c808007..832f7df8c 100644 --- a/app/src/main/res/menu/menu_nav.xml +++ b/app/src/main/res/menu/menu_nav.xml @@ -8,6 +8,10 @@ android:id="@+id/songs_fragment" android:icon="@drawable/ic_song" android:title="@string/label_songs" /> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_songs.xml b/app/src/main/res/menu/menu_songs.xml index ee30b099e..aa3007843 100644 --- a/app/src/main/res/menu/menu_songs.xml +++ b/app/src/main/res/menu/menu_songs.xml @@ -1,15 +1,6 @@ - - - + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e2880dfaf..834eba791 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -136,6 +136,7 @@ Turn shuffle off Change Repeat Mode Auxio icon + Clear search query Unknown Genre