577 lines
21 KiB
Kotlin
577 lines
21 KiB
Kotlin
package org.moire.ultrasonic.fragment
|
|
|
|
import android.app.SearchManager
|
|
import android.content.Context
|
|
import android.os.Bundle
|
|
import android.view.ContextMenu
|
|
import android.view.ContextMenu.ContextMenuInfo
|
|
import android.view.LayoutInflater
|
|
import android.view.Menu
|
|
import android.view.MenuInflater
|
|
import android.view.MenuItem
|
|
import android.view.View
|
|
import android.widget.AdapterView.AdapterContextMenuInfo
|
|
import android.widget.ListAdapter
|
|
import androidx.appcompat.widget.SearchView
|
|
import androidx.core.view.isVisible
|
|
import androidx.fragment.app.viewModels
|
|
import androidx.lifecycle.LiveData
|
|
import androidx.lifecycle.MutableLiveData
|
|
import androidx.lifecycle.viewModelScope
|
|
import androidx.navigation.Navigation
|
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
import kotlinx.coroutines.launch
|
|
import org.koin.core.component.KoinComponent
|
|
import org.koin.core.component.inject
|
|
import org.moire.ultrasonic.R
|
|
import org.moire.ultrasonic.adapters.AlbumRowBinder
|
|
import org.moire.ultrasonic.adapters.ArtistRowBinder
|
|
import org.moire.ultrasonic.adapters.DividerBinder
|
|
import org.moire.ultrasonic.adapters.TrackViewBinder
|
|
import org.moire.ultrasonic.domain.Identifiable
|
|
import org.moire.ultrasonic.domain.MusicDirectory
|
|
import org.moire.ultrasonic.domain.SearchResult
|
|
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
|
import org.moire.ultrasonic.model.SearchListModel
|
|
import org.moire.ultrasonic.service.MediaPlayerController
|
|
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
|
import org.moire.ultrasonic.subsonic.ShareHandler
|
|
import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo
|
|
import org.moire.ultrasonic.util.CancellationToken
|
|
import org.moire.ultrasonic.util.CommunicationError
|
|
import org.moire.ultrasonic.util.Constants
|
|
import org.moire.ultrasonic.util.Settings
|
|
import org.moire.ultrasonic.util.Util.toast
|
|
import org.moire.ultrasonic.view.ArtistAdapter
|
|
import timber.log.Timber
|
|
|
|
/**
|
|
* Initiates a search on the media library and displays the results
|
|
*/
|
|
class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|
private var moreArtistsButton: View? = null
|
|
private var moreAlbumsButton: View? = null
|
|
private var moreSongsButton: View? = null
|
|
private var searchResult: SearchResult? = null
|
|
private var artistAdapter: ArtistAdapter? = null
|
|
private var moreArtistsAdapter: ListAdapter? = null
|
|
private var moreAlbumsAdapter: ListAdapter? = null
|
|
private var moreSongsAdapter: ListAdapter? = null
|
|
private var searchRefresh: SwipeRefreshLayout? = null
|
|
|
|
private val mediaPlayerController: MediaPlayerController by inject()
|
|
|
|
private val shareHandler: ShareHandler by inject()
|
|
private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
|
|
|
|
private var cancellationToken: CancellationToken? = null
|
|
|
|
override val listModel: SearchListModel by viewModels()
|
|
|
|
override val mainLayout: Int = R.layout.search
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
super.onViewCreated(view, savedInstanceState)
|
|
cancellationToken = CancellationToken()
|
|
setTitle(this, R.string.search_title)
|
|
setHasOptionsMenu(true)
|
|
|
|
val buttons = LayoutInflater.from(context).inflate(
|
|
R.layout.search_buttons,
|
|
listView, false
|
|
)
|
|
|
|
if (buttons != null) {
|
|
moreArtistsButton = buttons.findViewById(R.id.search_more_artists)
|
|
moreAlbumsButton = buttons.findViewById(R.id.search_more_albums)
|
|
moreSongsButton = buttons.findViewById(R.id.search_more_songs)
|
|
}
|
|
|
|
listModel.searchResult.observe(
|
|
viewLifecycleOwner,
|
|
{
|
|
if (it != null) populateList(it)
|
|
}
|
|
)
|
|
|
|
searchRefresh = view.findViewById(R.id.swipe_refresh_view)
|
|
searchRefresh!!.isEnabled = false
|
|
|
|
// list.setOnItemClickListener(OnItemClickListener { parent: AdapterView<*>, view1: View, position: Int, id: Long ->
|
|
// if (view1 === moreArtistsButton) {
|
|
// expandArtists()
|
|
// } else if (view1 === moreAlbumsButton) {
|
|
// expandAlbums()
|
|
// } else if (view1 === moreSongsButton) {
|
|
// expandSongs()
|
|
// } else {
|
|
// val item = parent.getItemAtPosition(position)
|
|
// if (item is Artist) {
|
|
// onArtistSelected(item)
|
|
// } else if (item is MusicDirectory.Entry) {
|
|
// val entry = item
|
|
// if (entry.isDirectory) {
|
|
// onAlbumSelected(entry, false)
|
|
// } else if (entry.isVideo) {
|
|
// onVideoSelected(entry)
|
|
// } else {
|
|
// onSongSelected(entry, true)
|
|
// }
|
|
// }
|
|
// }
|
|
// })
|
|
|
|
registerForContextMenu(listView!!)
|
|
|
|
// Register our data binders
|
|
// IMPORTANT:
|
|
// They need to be added in the order of most specific -> least specific.
|
|
viewAdapter.register(
|
|
ArtistRowBinder(
|
|
onItemClick = { entry -> onItemClick(entry) },
|
|
onContextMenuClick = { menuItem, entry ->
|
|
onContextMenuItemSelected(
|
|
menuItem,
|
|
entry
|
|
)
|
|
},
|
|
imageLoader = imageLoaderProvider.getImageLoader(),
|
|
enableSections = false
|
|
)
|
|
)
|
|
|
|
viewAdapter.register(
|
|
AlbumRowBinder(
|
|
onItemClick = { entry -> onItemClick(entry) },
|
|
onContextMenuClick = { menuItem, entry ->
|
|
onContextMenuItemSelected(
|
|
menuItem,
|
|
entry
|
|
)
|
|
},
|
|
imageLoader = imageLoaderProvider.getImageLoader(),
|
|
context = requireContext()
|
|
)
|
|
)
|
|
|
|
viewAdapter.register(
|
|
TrackViewBinder(
|
|
checkable = false,
|
|
draggable = false,
|
|
context = requireContext(),
|
|
lifecycleOwner = viewLifecycleOwner
|
|
)
|
|
)
|
|
|
|
viewAdapter.register(
|
|
DividerBinder()
|
|
)
|
|
|
|
// Fragment was started with a query (e.g. from voice search), try to execute search right away
|
|
val arguments = arguments
|
|
if (arguments != null) {
|
|
val query = arguments.getString(Constants.INTENT_EXTRA_NAME_QUERY)
|
|
val autoPlay = arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)
|
|
if (query != null) {
|
|
return search(query, autoPlay)
|
|
}
|
|
}
|
|
|
|
// Fragment was started from the Menu, create empty list
|
|
// populateList(SearchResult())
|
|
}
|
|
|
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
val activity = activity ?: return
|
|
val searchManager = activity.getSystemService(Context.SEARCH_SERVICE) as SearchManager
|
|
inflater.inflate(R.menu.search, menu)
|
|
val searchItem = menu.findItem(R.id.search_item)
|
|
val searchView = searchItem.actionView as SearchView
|
|
val searchableInfo = searchManager.getSearchableInfo(requireActivity().componentName)
|
|
searchView.setSearchableInfo(searchableInfo)
|
|
|
|
val arguments = arguments
|
|
val autoPlay =
|
|
arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)
|
|
val query = arguments?.getString(Constants.INTENT_EXTRA_NAME_QUERY)
|
|
|
|
// If started with a query, enter it to the searchView
|
|
if (query != null) {
|
|
searchView.setQuery(query, false)
|
|
searchView.clearFocus()
|
|
}
|
|
|
|
searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener {
|
|
override fun onSuggestionSelect(position: Int): Boolean {
|
|
return true
|
|
}
|
|
|
|
override fun onSuggestionClick(position: Int): Boolean {
|
|
Timber.d("onSuggestionClick: %d", position)
|
|
val cursor = searchView.suggestionsAdapter.cursor
|
|
cursor.moveToPosition(position)
|
|
|
|
// TODO: Try to do something with this magic const:
|
|
// 2 is the index of col containing suggestion name.
|
|
val suggestion = cursor.getString(2)
|
|
searchView.setQuery(suggestion, true)
|
|
return true
|
|
}
|
|
})
|
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
|
override fun onQueryTextSubmit(query: String): Boolean {
|
|
Timber.d("onQueryTextSubmit: %s", query)
|
|
searchView.clearFocus()
|
|
search(query, autoPlay)
|
|
return true
|
|
}
|
|
|
|
override fun onQueryTextChange(newText: String): Boolean {
|
|
return true
|
|
}
|
|
})
|
|
searchView.setIconifiedByDefault(false)
|
|
searchItem.expandActionView()
|
|
}
|
|
|
|
// FIXME
|
|
override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
|
|
super.onCreateContextMenu(menu, view, menuInfo)
|
|
if (activity == null) return
|
|
val info = menuInfo as AdapterContextMenuInfo?
|
|
// val selectedItem = list!!.getItemAtPosition(info!!.position)
|
|
// val isArtist = selectedItem is Artist
|
|
// val isAlbum = selectedItem is MusicDirectory.Entry && selectedItem.isDirectory
|
|
// val inflater = requireActivity().menuInflater
|
|
// if (!isArtist && !isAlbum) {
|
|
// inflater.inflate(R.menu.select_song_context, menu)
|
|
// } else {
|
|
// inflater.inflate(R.menu.generic_context_menu, menu)
|
|
// }
|
|
// val shareButton = menu.findItem(R.id.menu_item_share)
|
|
// val downloadMenuItem = menu.findItem(R.id.menu_download)
|
|
// if (downloadMenuItem != null) {
|
|
// downloadMenuItem.isVisible = !isOffline()
|
|
// }
|
|
// if (isOffline() || isArtist) {
|
|
// if (shareButton != null) {
|
|
// shareButton.isVisible = false
|
|
// }
|
|
// }
|
|
}
|
|
|
|
// FIXME
|
|
override fun onContextItemSelected(menuItem: MenuItem): Boolean {
|
|
val info = menuItem.menuInfo as AdapterContextMenuInfo
|
|
// val selectedItem = list!!.getItemAtPosition(info.position)
|
|
// val artist = if (selectedItem is Artist) selectedItem else null
|
|
// val entry = if (selectedItem is MusicDirectory.Entry) selectedItem else null
|
|
// var entryId: String? = null
|
|
// if (entry != null) {
|
|
// entryId = entry.id
|
|
// }
|
|
// val id = artist?.id ?: entryId ?: return true
|
|
// var songs: MutableList<MusicDirectory.Entry?> = ArrayList(1)
|
|
// val itemId = menuItem.itemId
|
|
// if (itemId == R.id.menu_play_now) {
|
|
// downloadHandler.downloadRecursively(
|
|
// this,
|
|
// id,
|
|
// false,
|
|
// false,
|
|
// true,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// false
|
|
// )
|
|
// } else if (itemId == R.id.menu_play_next) {
|
|
// downloadHandler.downloadRecursively(
|
|
// this,
|
|
// id,
|
|
// false,
|
|
// true,
|
|
// false,
|
|
// true,
|
|
// false,
|
|
// true,
|
|
// false,
|
|
// false
|
|
// )
|
|
// } else if (itemId == R.id.menu_play_last) {
|
|
// downloadHandler.downloadRecursively(
|
|
// this,
|
|
// id,
|
|
// false,
|
|
// true,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// false
|
|
// )
|
|
// } else if (itemId == R.id.menu_pin) {
|
|
// downloadHandler.downloadRecursively(
|
|
// this,
|
|
// id,
|
|
// true,
|
|
// true,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// false
|
|
// )
|
|
// } else if (itemId == R.id.menu_unpin) {
|
|
// downloadHandler.downloadRecursively(
|
|
// this,
|
|
// id,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// true,
|
|
// false
|
|
// )
|
|
// } else if (itemId == R.id.menu_download) {
|
|
// downloadHandler.downloadRecursively(
|
|
// this,
|
|
// id,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// false,
|
|
// true,
|
|
// false,
|
|
// false,
|
|
// false
|
|
// )
|
|
// } else if (itemId == R.id.song_menu_play_now) {
|
|
// if (entry != null) {
|
|
// songs = ArrayList(1)
|
|
// songs.add(entry)
|
|
// downloadHandler.download(this, false, false, true, false, false, songs)
|
|
// }
|
|
// } else if (itemId == R.id.song_menu_play_next) {
|
|
// if (entry != null) {
|
|
// songs = ArrayList(1)
|
|
// songs.add(entry)
|
|
// downloadHandler.download(this, true, false, false, true, false, songs)
|
|
// }
|
|
// } else if (itemId == R.id.song_menu_play_last) {
|
|
// if (entry != null) {
|
|
// songs = ArrayList(1)
|
|
// songs.add(entry)
|
|
// downloadHandler.download(this, true, false, false, false, false, songs)
|
|
// }
|
|
// } else if (itemId == R.id.song_menu_pin) {
|
|
// if (entry != null) {
|
|
// songs.add(entry)
|
|
// toast(
|
|
// context,
|
|
// resources.getQuantityString(
|
|
// R.plurals.select_album_n_songs_pinned,
|
|
// songs.size,
|
|
// songs.size
|
|
// )
|
|
// )
|
|
// downloadBackground(true, songs)
|
|
// }
|
|
// } else if (itemId == R.id.song_menu_download) {
|
|
// if (entry != null) {
|
|
// songs.add(entry)
|
|
// toast(
|
|
// context,
|
|
// resources.getQuantityString(
|
|
// R.plurals.select_album_n_songs_downloaded,
|
|
// songs.size,
|
|
// songs.size
|
|
// )
|
|
// )
|
|
// downloadBackground(false, songs)
|
|
// }
|
|
// } else if (itemId == R.id.song_menu_unpin) {
|
|
// if (entry != null) {
|
|
// songs.add(entry)
|
|
// toast(
|
|
// context,
|
|
// resources.getQuantityString(
|
|
// R.plurals.select_album_n_songs_unpinned,
|
|
// songs.size,
|
|
// songs.size
|
|
// )
|
|
// )
|
|
// mediaPlayerController.unpin(songs)
|
|
// }
|
|
// } else if (itemId == R.id.menu_item_share) {
|
|
// if (entry != null) {
|
|
// songs = ArrayList(1)
|
|
// songs.add(entry)
|
|
// shareHandler.createShare(this, songs, searchRefresh, cancellationToken!!)
|
|
// }
|
|
// return super.onContextItemSelected(menuItem)
|
|
// } else {
|
|
// return super.onContextItemSelected(menuItem)
|
|
// }
|
|
return true
|
|
}
|
|
|
|
// OK!
|
|
override fun onDestroyView() {
|
|
cancellationToken?.cancel()
|
|
super.onDestroyView()
|
|
}
|
|
|
|
// OK!
|
|
private fun downloadBackground(save: Boolean, songs: List<MusicDirectory.Entry?>) {
|
|
val onValid = Runnable {
|
|
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
|
mediaPlayerController.downloadBackground(songs, save)
|
|
}
|
|
onValid.run()
|
|
}
|
|
|
|
private fun search(query: String, autoplay: Boolean) {
|
|
// FIXME support autoplay
|
|
listModel.viewModelScope.launch(CommunicationError.getHandler(context)) {
|
|
refreshListView?.isRefreshing = true
|
|
listModel.search(query)
|
|
refreshListView?.isRefreshing = false
|
|
}
|
|
}
|
|
|
|
private fun populateList(result: SearchResult) {
|
|
val searchResult = listModel.trimResultLength(result)
|
|
|
|
val list = mutableListOf<Identifiable>()
|
|
|
|
val artists = searchResult.artists
|
|
if (artists.isNotEmpty()) {
|
|
|
|
list.add(DividerBinder.Divider(R.string.search_artists))
|
|
list.addAll(artists)
|
|
if (artists.size > DEFAULT_ARTISTS) {
|
|
// FIXME
|
|
// list.add((moreArtistsButton, true)
|
|
}
|
|
}
|
|
val albums = searchResult.albums
|
|
if (albums.isNotEmpty()) {
|
|
list.add(DividerBinder.Divider(R.string.search_albums))
|
|
list.addAll(albums)
|
|
// mergeAdapter!!.addAdapter(albumAdapter)
|
|
// if (albums.size > DEFAULT_ALBUMS) {
|
|
// moreAlbumsAdapter = mergeAdapter!!.addView(moreAlbumsButton, true)
|
|
// }
|
|
}
|
|
val songs = searchResult.songs
|
|
if (songs.isNotEmpty()) {
|
|
list.add(DividerBinder.Divider(R.string.search_songs))
|
|
list.addAll(songs)
|
|
// if (songs.size > DEFAULT_SONGS) {
|
|
// moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true)
|
|
// }
|
|
}
|
|
|
|
// Show/hide the empty text view
|
|
emptyTextView.isVisible = list.isEmpty()
|
|
|
|
viewAdapter.submitList(list)
|
|
}
|
|
|
|
// private fun expandArtists() {
|
|
// artistAdapter!!.clear()
|
|
// for (artist in searchResult!!.artists) {
|
|
// artistAdapter!!.add(artist)
|
|
// }
|
|
// artistAdapter!!.notifyDataSetChanged()
|
|
// mergeAdapter!!.removeAdapter(moreArtistsAdapter)
|
|
// mergeAdapter!!.notifyDataSetChanged()
|
|
// }
|
|
//
|
|
// private fun expandAlbums() {
|
|
// albumAdapter!!.clear()
|
|
// for (album in searchResult!!.albums) {
|
|
// albumAdapter!!.add(album)
|
|
// }
|
|
// albumAdapter!!.notifyDataSetChanged()
|
|
// mergeAdapter!!.removeAdapter(moreAlbumsAdapter)
|
|
// mergeAdapter!!.notifyDataSetChanged()
|
|
// }
|
|
//
|
|
// private fun expandSongs() {
|
|
// songAdapter!!.clear()
|
|
// for (song in searchResult!!.songs) {
|
|
// songAdapter!!.add(song)
|
|
// }
|
|
// songAdapter!!.notifyDataSetChanged()
|
|
// mergeAdapter!!.removeAdapter(moreSongsAdapter)
|
|
// mergeAdapter!!.notifyDataSetChanged()
|
|
// }
|
|
//
|
|
// private fun onArtistSelected(artist: Artist) {
|
|
// val bundle = Bundle()
|
|
// bundle.putString(Constants.INTENT_EXTRA_NAME_ID, artist.id)
|
|
// bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.id)
|
|
// Navigation.findNavController(requireView()).navigate(R.id.searchToSelectAlbum, bundle)
|
|
// }
|
|
|
|
private fun onAlbumSelected(album: MusicDirectory.Album, autoplay: Boolean) {
|
|
val bundle = Bundle()
|
|
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, album.id)
|
|
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, album.title)
|
|
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, album.isDirectory)
|
|
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoplay)
|
|
Navigation.findNavController(requireView()).navigate(R.id.searchToSelectAlbum, bundle)
|
|
}
|
|
|
|
private fun onSongSelected(song: MusicDirectory.Entry, append: Boolean) {
|
|
if (!append) {
|
|
mediaPlayerController.clear()
|
|
}
|
|
mediaPlayerController.addToPlaylist(listOf(song), false, false, false, false, false)
|
|
mediaPlayerController.play(mediaPlayerController.playlistSize - 1)
|
|
toast(context, resources.getQuantityString(R.plurals.select_album_n_songs_added, 1, 1))
|
|
}
|
|
|
|
private fun onVideoSelected(entry: MusicDirectory.Entry) {
|
|
playVideo(requireContext(), entry)
|
|
}
|
|
|
|
private fun autoplay() {
|
|
if (searchResult!!.songs.isNotEmpty()) {
|
|
onSongSelected(searchResult!!.songs[0], false)
|
|
} else if (searchResult!!.albums.isNotEmpty()) {
|
|
onAlbumSelected(searchResult!!.albums[0], true)
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
var DEFAULT_ARTISTS = Settings.defaultArtists
|
|
var DEFAULT_ALBUMS = Settings.defaultAlbums
|
|
var DEFAULT_SONGS = Settings.defaultSongs
|
|
}
|
|
|
|
// FIXME!!
|
|
override fun getLiveData(args: Bundle?): LiveData<List<Identifiable>> {
|
|
return MutableLiveData(listOf())
|
|
}
|
|
|
|
// FIXME
|
|
override val itemClickTarget: Int = 0
|
|
|
|
// FIXME
|
|
override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean {
|
|
return true
|
|
}
|
|
|
|
// FIXME
|
|
override fun onItemClick(item: Identifiable) {
|
|
}
|
|
}
|