Move search button in to main Topbar menu

This commit is contained in:
birdbird 2023-06-14 12:07:13 +00:00
parent 8b9dc294c1
commit 725d9281bf
28 changed files with 144 additions and 195 deletions

View File

@ -2,7 +2,7 @@
# You need to run ./gradlew wrapper after updating the version
gradle = "8.1.1"
navigation = "2.5.3"
navigation = "2.6.0"
gradlePlugin = "8.0.2"
androidxcore = "1.10.1"
ktlint = "0.43.2"

View File

@ -1,37 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2010 (C) Sindre Mehus
*/
package org.moire.ultrasonic.provider;
import android.content.SearchRecentSuggestionsProvider;
/**
* Provides search suggestions based on recent searches.
*
* @author Sindre Mehus
*/
public class SearchSuggestionProvider extends SearchRecentSuggestionsProvider
{
public static final String AUTHORITY = SearchSuggestionProvider.class.getName();
public static final int MODE = DATABASE_MODE_QUERIES;
public SearchSuggestionProvider()
{
setupSuggestions(AUTHORITY, MODE);
}
}

View File

@ -18,16 +18,20 @@ import android.os.Bundle
import android.provider.MediaStore
import android.provider.SearchRecentSuggestions
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.view.MenuProvider
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import androidx.media3.common.Player.STATE_BUFFERING
@ -94,6 +98,10 @@ class NavigationActivity : AppCompatActivity() {
private var selectServerButton: MaterialButton? = null
private var headerBackgroundImage: ImageView? = null
// We store the last search string in this variable.
// Seems a bit like a hack, is there a better way?
var searchQuery: String? = null
private lateinit var appBarConfiguration: AppBarConfiguration
private var rxBusSubscription: CompositeDisposable = CompositeDisposable()
@ -221,10 +229,57 @@ class NavigationActivity : AppCompatActivity() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && !UApp.instance!!.isFirstRun) {
ShortcutUtil.registerShortcuts(this)
}
// Register our options menu
addMenuProvider(
searchMenuProvider,
this,
Lifecycle.State.RESUMED
)
}
private val searchMenuProvider: MenuProvider = object : MenuProvider {
override fun onPrepareMenu(menu: Menu) {
setupSearchField(menu)
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.search_view_menu, menu)
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
return false
}
}
fun setupSearchField(menu: Menu) {
Timber.i("Recreating search field")
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
val searchableInfo = searchManager.getSearchableInfo(this.componentName)
searchView.setSearchableInfo(searchableInfo)
searchView.setIconifiedByDefault(false)
if (searchQuery != null) {
Timber.e("Found existing search query")
searchItem.expandActionView()
searchView.isIconified = false
searchView.setQuery(searchQuery, false)
searchView.clearFocus()
// Restore search text only once!
searchQuery = null
}
}
private fun setupDrawerLayout(drawerLayout: DrawerLayout) {
// Set initial state passed on drawer state
closeNavigationDrawerOnBack.isEnabled = drawerLayout.isOpen
// Add the back press listener
onBackPressedDispatcher.addCallback(this, closeNavigationDrawerOnBack)
// Listen to changes in the drawer state and enable the back press listener accordingly.
drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener {
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
// Nothing
@ -392,47 +447,55 @@ class NavigationActivity : AppCompatActivity() {
return findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration)
}
// TODO: Test if this works with external Intents
// android.intent.action.SEARCH and android.media.action.MEDIA_PLAY_FROM_SEARCH calls here
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (intent == null) return
if (intent.action == Constants.INTENT_PLAY_RANDOM_SONGS) {
val currentFragment = host?.childFragmentManager?.fragments?.last() ?: return
val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getRandomSongs(Settings.maxSongs)
val downloadHandler: DownloadHandler by inject()
downloadHandler.addTracksToMediaController(
songs = musicDirectory.getTracks(),
append = false,
playNext = false,
autoPlay = true,
shuffle = false,
fragment = currentFragment,
playlistName = null
)
return
when (intent?.action) {
Constants.INTENT_PLAY_RANDOM_SONGS -> {
playRandomSongs()
}
Intent.ACTION_MAIN -> {
if (intent.getBooleanExtra(Constants.INTENT_SHOW_PLAYER, false)) {
findNavController(R.id.nav_host_fragment).navigate(R.id.playerFragment)
}
}
Intent.ACTION_SEARCH -> {
searchQuery = intent.getStringExtra(SearchManager.QUERY)
handleSearchIntent(searchQuery, false)
}
MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH -> {
searchQuery = intent.getStringExtra(SearchManager.QUERY)
handleSearchIntent(searchQuery, true)
}
}
}
if (intent.getBooleanExtra(Constants.INTENT_SHOW_PLAYER, false)) {
findNavController(R.id.nav_host_fragment).navigate(R.id.playerFragment)
return
}
private fun handleSearchIntent(query: String?, autoPlay: Boolean) {
val suggestions = SearchRecentSuggestions(
this,
SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE
)
suggestions.saveRecentQuery(query, null)
val query = intent.getStringExtra(SearchManager.QUERY)
val action = NavigationGraphDirections.toSearchFragment(query, autoPlay)
findNavController(R.id.nav_host_fragment).navigate(action)
}
if (query != null) {
val autoPlay = intent.action == MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
val suggestions = SearchRecentSuggestions(
this,
SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE
)
suggestions.saveRecentQuery(query, null)
val action = NavigationGraphDirections.toSearchFragment(query, autoPlay)
findNavController(R.id.nav_host_fragment).navigate(action)
}
private fun playRandomSongs() {
val currentFragment = host?.childFragmentManager?.fragments?.last() ?: return
val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getRandomSongs(Settings.maxSongs)
val downloadHandler: DownloadHandler by inject()
downloadHandler.addTracksToMediaController(
songs = musicDirectory.getTracks(),
append = false,
playNext = false,
autoPlay = true,
shuffle = false,
fragment = currentFragment,
playlistName = null
)
return
}
/**

View File

@ -7,19 +7,11 @@
package org.moire.ultrasonic.fragment
import android.app.SearchManager
import android.content.Context
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@ -54,16 +46,14 @@ import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.Util.toast
import timber.log.Timber
/**
* Initiates a search on the media library and displays the results
* TODO: Switch to material3 class
*/
class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
private var searchResult: SearchResult? = null
private var searchRefresh: SwipeRefreshLayout? = null
private var searchView: SearchView? = null
private val mediaPlayerManager: MediaPlayerManager by inject()
@ -83,13 +73,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
cancellationToken = CancellationToken()
setTitle(this, R.string.search_title)
// Register our options menu
(requireActivity() as MenuHost).addMenuProvider(
menuProvider,
viewLifecycleOwner,
Lifecycle.State.RESUMED
)
listModel.searchResult.observe(
viewLifecycleOwner
) {
@ -148,73 +131,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
}
}
/**
* This provide creates the search bar above the recycler view
*/
private val menuProvider: MenuProvider = object : MenuProvider {
override fun onPrepareMenu(menu: Menu) {
setupOptionsMenu(menu)
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.search, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return true
}
}
fun setupOptionsMenu(menu: Menu) {
val activity = activity ?: return
val searchManager = activity.getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchItem = menu.findItem(R.id.search_item)
searchView = searchItem.actionView as SearchView
val searchableInfo = searchManager.getSearchableInfo(requireActivity().componentName)
searchView!!.setSearchableInfo(searchableInfo)
val autoPlay = navArgs.autoplay
val query = navArgs.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)
// 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()
}
override fun onDestroyView() {
Util.hideKeyboard(activity)
cancellationToken?.cancel()
@ -313,7 +229,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
}
private fun onAlbumSelected(album: Album, autoplay: Boolean) {
val action = SearchFragmentDirections.searchToTrackCollection(
id = album.id,
name = album.title,

View File

@ -36,7 +36,6 @@ import org.moire.ultrasonic.adapters.AlbumHeader
import org.moire.ultrasonic.adapters.AlbumRowDelegate
import org.moire.ultrasonic.adapters.HeaderViewBinder
import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory
@ -64,7 +63,6 @@ import timber.log.Timber
* In most cases the data should be just a list of Entries, but there are some cases
* where the list can contain Albums as well. This happens especially when having ID3 tags disabled,
* or using Offline mode, both in which Indexes instead of Artists are being used.
*
*/
@Suppress("TooManyFunctions")
open class TrackCollectionFragment(
@ -268,6 +266,9 @@ open class TrackCollectionFragment(
private val menuProvider: MenuProvider = object : MenuProvider {
override fun onPrepareMenu(menu: Menu) {
// Hide search button (from xml)
menu.findItem(R.id.action_search).isVisible = false
playAllButton = menu.findItem(R.id.select_album_play_all)
if (playAllButton != null) {
@ -282,7 +283,7 @@ open class TrackCollectionFragment(
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.select_album, menu)
inflater.inflate(R.menu.track_collection_menu, menu)
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
@ -584,12 +585,8 @@ open class TrackCollectionFragment(
} else {
setTitle(name)
if (ActiveServerProvider.shouldUseId3Tags()) {
if (isAlbum) {
listModel.getAlbum(refresh2, id, name)
} else {
throw IllegalAccessException("Use AlbumFragment instead!")
}
if (isAlbum) {
listModel.getAlbum(refresh2, id, name)
} else {
listModel.getMusicDirectory(refresh2, id, name)
}

View File

@ -340,6 +340,7 @@ class PlaybackService :
// needed starting Android 12 (S = 31)
flags = flags or FLAG_IMMUTABLE
}
intent.action = Intent.ACTION_MAIN
intent.putExtra(Constants.INTENT_SHOW_PLAYER, true)
return PendingIntent.getActivity(this, 0, intent, flags)
}

View File

@ -0,0 +1,24 @@
/*
* SearchSuggestionProvider.kt
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.provider
import android.content.SearchRecentSuggestionsProvider
import org.moire.ultrasonic.BuildConfig
/**
* Provides search suggestions based on recent searches.
*/
class SearchSuggestionProvider : SearchRecentSuggestionsProvider() {
init {
setupSuggestions(AUTHORITY, MODE)
}
companion object {
val AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider.SearchSuggestionProvider"
const val MODE = DATABASE_MODE_QUERIES
}
}

View File

@ -219,8 +219,8 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
NavigationActivity::class.java
).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
if (playerActive) intent.putExtra(Constants.INTENT_SHOW_PLAYER, true)
intent.action = "android.intent.action.MAIN"
intent.addCategory("android.intent.category.LAUNCHER")
intent.action = Intent.ACTION_MAIN
intent.addCategory(Intent.CATEGORY_LAUNCHER)
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// needed starting Android 12 (S = 31)

View File

@ -30,7 +30,6 @@ object Constants {
// Legacy Preferences keys
// Warning: Don't add any new here!
// Use setting_keys.xml
const val PREFERENCES_KEY_USE_FIVE_STAR_RATING = "use_five_star_rating"
const val PREFERENCE_VALUE_ALL = 0
const val PREFERENCE_VALUE_A2DP = 1
const val PREFERENCE_VALUE_DISABLED = 2

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:a="http://schemas.android.com/apk/res/android">
<item a:id="@+id/search_item"
a:title="@string/search.label"
a:icon="@drawable/ic_menu_search"
app:showAsAction="always"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
a:id="@+id/action_search"
a:icon="@drawable/ic_menu_search"
a:title="@string/button_bar.search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always|collapseActionView" />
</menu>

View File

@ -104,7 +104,6 @@
<string name="playlist.updated_info_error">Chyba aktualizace informací playlistu %s</string>
<string name="search.albums">Alba</string>
<string name="search.artists">Umělci</string>
<string name="search.label">Vyhledávání</string>
<string name="search.more">Zobrazit více</string>
<string name="search.no_match">Nenalezeno, zkuste znovu</string>
<string name="search.songs">Skladby</string>

View File

@ -139,7 +139,6 @@
<string name="playlist.updated_info_error">Aktualisierung der Wiedergabeliste %s ist fehlgeschlagen</string>
<string name="search.albums">Alben</string>
<string name="search.artists">Künstler*innen</string>
<string name="search.label">Suche</string>
<string name="search.more">Zeige mehr</string>
<string name="search.no_match">Keine Treffer, bitte erneut versuchen</string>
<string name="search.songs">Titel</string>

View File

@ -141,7 +141,6 @@
<string name="playlist.updated_info_error">Fallo al actualizar la información de la lista de reproducción para %s</string>
<string name="search.albums">Álbumes</string>
<string name="search.artists">Artistas</string>
<string name="search.label">Buscar</string>
<string name="search.more">Mostrar mas</string>
<string name="search.no_match">Sin resultados, por favor inténtalo de nuevo</string>
<string name="search.songs">Canciones</string>

View File

@ -137,7 +137,6 @@
<string name="playlist.updated_info_error">Échec de la mise à jour des informations de la liste de lecture pour %s</string>
<string name="search.albums">Albums</string>
<string name="search.artists">Artistes</string>
<string name="search.label">Recherche</string>
<string name="search.more">Afficher plus</string>
<string name="search.no_match">Aucun résultat, veuillez essayer à nouveau</string>
<string name="search.songs">Titres</string>

View File

@ -110,7 +110,6 @@
<string name="playlist.updated_info_error">Lejátszási lista módosítása sikertelen %s</string>
<string name="search.albums">Albumok</string>
<string name="search.artists">Előadók</string>
<string name="search.label">Keresés</string>
<string name="search.more">Továbbiak</string>
<string name="search.no_match">Nincs találat, próbálja újra!</string>
<string name="search.songs">Dalok</string>

View File

@ -100,7 +100,6 @@
<string name="playlist.updated_info_error">Impossibile aggiornare informazioni playlist per %s</string>
<string name="search.albums">Album</string>
<string name="search.artists">Artisti</string>
<string name="search.label">Cerca</string>
<string name="search.more">Mostra di più</string>
<string name="search.no_match">Nessun risultato, riprova per favore</string>
<string name="search.songs">Canzoni</string>

View File

@ -106,7 +106,6 @@
<string name="playlist.updated_info_error">%s のプレイリスト情報をアップデートできません</string>
<string name="search.albums">アルバム</string>
<string name="search.artists">アーティスト</string>
<string name="search.label">検索</string>
<string name="search.more">もっと表示</string>
<string name="search.no_match">一致するものはありません、やり直してください</string>
<string name="search.songs"></string>

View File

@ -186,7 +186,6 @@
<string name="playlist.update_info">Oppdater info</string>
<string name="search.albums">Album</string>
<string name="search.artists">Artister</string>
<string name="search.label">Søk</string>
<string name="search.more">Vis mer</string>
<string name="search.songs">Spor</string>
<string name="search.title">Søk</string>

View File

@ -142,7 +142,6 @@
<string name="playlist.updated_info_error">Kan afspeellijstinformatie voor %s niet bijwerken</string>
<string name="search.albums">Albums</string>
<string name="search.artists">Artiesten</string>
<string name="search.label">Zoeken</string>
<string name="search.more">Meer tonen</string>
<string name="search.no_match">Geen overeenkomsten; probeer het opnieuw</string>
<string name="search.songs">Nummers</string>

View File

@ -103,7 +103,6 @@
<string name="playlist.updated_info_error">Błąd podczas aktualizacji playlisty %s</string>
<string name="search.albums">Albumy</string>
<string name="search.artists">Wykonawcy</string>
<string name="search.label">Wyszukaj</string>
<string name="search.more">Wyświetl więcej</string>
<string name="search.no_match">Brak wyników, proszę spróbować ponownie</string>
<string name="search.songs">Utwory</string>

View File

@ -140,7 +140,6 @@
<string name="playlist.updated_info_error">Falha ao atualizar a informação da playlist para %s</string>
<string name="search.albums">Álbuns</string>
<string name="search.artists">Artistas</string>
<string name="search.label">Pesquisar</string>
<string name="search.more">Mostrar Mais</string>
<string name="search.no_match">Nada coincide, tente novamente</string>
<string name="search.songs">Músicas</string>

View File

@ -103,7 +103,6 @@
<string name="playlist.updated_info_error">Falha ao atualizar a informação da playlist para %s</string>
<string name="search.albums">Álbuns</string>
<string name="search.artists">Artistas</string>
<string name="search.label">Pesquisar</string>
<string name="search.more">Mostrar Mais</string>
<string name="search.no_match">Nada coincide, tente novamente</string>
<string name="search.songs">Músicas</string>

View File

@ -128,7 +128,6 @@
<string name="playlist.updated_info_error">Не удалось обновить информацию о плейлисте для %s</string>
<string name="search.albums">Альбомы</string>
<string name="search.artists">Исполнители</string>
<string name="search.label">Поиск</string>
<string name="search.more">Показать еще</string>
<string name="search.no_match">Нет совпадений, пожалуйста попробуйте еще раз</string>
<string name="search.songs">Песни</string>

View File

@ -128,7 +128,6 @@
<string name="playlist.updated_info_error">更新播放列表信息失败 - %s</string>
<string name="search.albums">专辑</string>
<string name="search.artists">艺人</string>
<string name="search.label">搜索</string>
<string name="search.more">显示更多</string>
<string name="search.no_match">没有匹配的结果,请重试</string>
<string name="search.songs">歌曲</string>

View File

@ -67,7 +67,6 @@
<string name="playlist.label">播放清單</string>
<string name="search.albums">專輯</string>
<string name="search.artists">歌手</string>
<string name="search.label">搜尋</string>
<string name="search.title">搜尋</string>
<string name="select_genre.empty">無符合類型</string>
<string name="settings.increment_time_0">已停用</string>

View File

@ -142,7 +142,6 @@
<string name="playlist.updated_info_error">Failed to update playlist information for %s</string>
<string name="search.albums">Albums</string>
<string name="search.artists">Artists</string>
<string name="search.label">Search</string>
<string name="search.more">Show More</string>
<string name="search.no_match">No matches, please try again</string>
<string name="search.songs">Songs</string>