Start search implementation

This commit is contained in:
Kirill Kamakin
2022-11-12 15:26:57 +01:00
parent effd4934a5
commit 21abf38282
15 changed files with 104 additions and 23 deletions

View File

@@ -19,12 +19,21 @@
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<activity <activity
android:name=".ui.activity.MainActivity" android:name=".ui.activity.MainActivity"
android:launchMode="singleTop"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable_recipe_main" />
</activity> </activity>
</application> </application>

View File

@@ -1,6 +1,5 @@
package gq.kirmanak.mealient.extensions package gq.kirmanak.mealient.extensions
import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -18,10 +17,7 @@ fun <T> Fragment.collectWhenViewResumed(flow: Flow<T>, collector: FlowCollector<
} }
} }
fun Fragment.showLongToast(@StringRes text: Int) = showLongToast(getString(text)) fun Fragment.showLongToast(@StringRes text: Int) = context?.showLongToast(text) != null
fun Fragment.showLongToast(text: String) = showToast(text, Toast.LENGTH_LONG) fun Fragment.showLongToast(text: String) = context?.showLongToast(text) != null
private fun Fragment.showToast(text: String, length: Int): Boolean {
return context?.let { Toast.makeText(it, text, length).show() } != null
}

View File

@@ -1,8 +1,10 @@
package gq.kirmanak.mealient.extensions package gq.kirmanak.mealient.extensions
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@@ -96,3 +98,12 @@ fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observ
} }
}) })
} }
fun Context.showLongToast(text: String) = showToast(text, Toast.LENGTH_LONG)
fun Context.showLongToast(@StringRes text: Int) = showLongToast(getString(text))
private fun Context.showToast(text: String, length: Int) {
Toast.makeText(this, text, length).show()
}

View File

@@ -1,10 +1,14 @@
package gq.kirmanak.mealient.ui.activity package gq.kirmanak.mealient.ui.activity
import android.app.SearchManager
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.content.getSystemService
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.navigation.NavController import androidx.navigation.NavController
@@ -46,6 +50,15 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
binding.navigationView.setNavigationItemSelectedListener(::onNavigationItemSelected) binding.navigationView.setNavigationItemSelectedListener(::onNavigationItemSelected)
} }
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (Intent.ACTION_SEARCH == intent?.action) {
intent.getStringExtra(SearchManager.QUERY)?.also { query ->
viewModel.onSearchQuery(query)
}
}
}
private fun configureNavGraph() { private fun configureNavGraph() {
viewModel.startDestination.observeOnce(this) { viewModel.startDestination.observeOnce(this) {
logger.d { "configureNavGraph: received destination" } logger.d { "configureNavGraph: received destination" }
@@ -104,6 +117,15 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
menuInflater.inflate(R.menu.main_toolbar, menu) menuInflater.inflate(R.menu.main_toolbar, menu)
menu.findItem(R.id.logout).isVisible = uiState.canShowLogout menu.findItem(R.id.logout).isVisible = uiState.canShowLogout
menu.findItem(R.id.login).isVisible = uiState.canShowLogin menu.findItem(R.id.login).isVisible = uiState.canShowLogin
val searchItem = menu.findItem(R.id.search_recipe_action)
searchItem.isVisible = uiState.searchVisible
val searchManager: SearchManager? = getSystemService()
val searchView = searchItem.actionView as? SearchView
if (searchManager != null && searchView != null) {
searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName))
} else {
logger.e { "onCreateOptionsMenu: either search manager or search view is null" }
}
return true return true
} }

View File

@@ -5,6 +5,7 @@ data class MainActivityUiState(
val titleVisible: Boolean = true, val titleVisible: Boolean = true,
val isAuthorized: Boolean = false, val isAuthorized: Boolean = false,
val navigationVisible: Boolean = false, val navigationVisible: Boolean = false,
val searchVisible: Boolean = false,
) { ) {
val canShowLogin: Boolean val canShowLogin: Boolean
get() = !isAuthorized && loginButtonVisible get() = !isAuthorized && loginButtonVisible

View File

@@ -52,4 +52,8 @@ class MainActivityViewModel @Inject constructor(
logger.v { "logout() called" } logger.v { "logout() called" }
viewModelScope.launch { authRepo.logout() } viewModelScope.launch { authRepo.logout() }
} }
fun onSearchQuery(query: String) {
logger.v { "onSearchQuery() called with: query = $query" }
}
} }

View File

@@ -38,7 +38,12 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
activityViewModel.updateUiState { activityViewModel.updateUiState {
it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true) it.copy(
loginButtonVisible = true,
titleVisible = false,
navigationVisible = true,
searchVisible = false,
)
} }
viewModel.loadPreservedRequest() viewModel.loadPreservedRequest()
setupViews() setupViews()

View File

@@ -32,7 +32,12 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
binding.button.setOnClickListener { onLoginClicked() } binding.button.setOnClickListener { onLoginClicked() }
activityViewModel.updateUiState { activityViewModel.updateUiState {
it.copy(loginButtonVisible = false, titleVisible = true, navigationVisible = false) it.copy(
loginButtonVisible = false,
titleVisible = true,
navigationVisible = false,
searchVisible = false
)
} }
viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange) viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange)
} }

View File

@@ -33,7 +33,12 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
binding.button.setOnClickListener(::onProceedClick) binding.button.setOnClickListener(::onProceedClick)
viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange) viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange)
activityViewModel.updateUiState { activityViewModel.updateUiState {
it.copy(loginButtonVisible = false, titleVisible = true, navigationVisible = false) it.copy(
loginButtonVisible = false,
titleVisible = true,
navigationVisible = false,
searchVisible = false
)
} }
} }

View File

@@ -57,7 +57,12 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) {
} }
viewModel.startCountDown() viewModel.startCountDown()
activityViewModel.updateUiState { activityViewModel.updateUiState {
it.copy(loginButtonVisible = false, titleVisible = true, navigationVisible = false) it.copy(
loginButtonVisible = false,
titleVisible = true,
navigationVisible = false,
searchVisible = false
)
} }
} }
} }

View File

@@ -50,7 +50,12 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
activityViewModel.updateUiState { activityViewModel.updateUiState {
it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true) it.copy(
loginButtonVisible = true,
titleVisible = false,
navigationVisible = true,
searchVisible = true,
)
} }
setupRecipeAdapter() setupRecipeAdapter()
} }

View File

@@ -14,4 +14,11 @@
android:title="@string/menu_main_toolbar_logout" android:title="@string/menu_main_toolbar_logout"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/search_recipe_action"
android:icon="@android:drawable/ic_menu_search"
android:title="@string/searchable_recipe_main_hint"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom" />
</menu> </menu>

View File

@@ -48,4 +48,5 @@
<string name="fragment_recipes_load_failure_toast_no_connection">нет соединения</string> <string name="fragment_recipes_load_failure_toast_no_connection">нет соединения</string>
<string name="fragment_recipes_load_failure_toast_no_reason">Ошибка загрузки.</string> <string name="fragment_recipes_load_failure_toast_no_reason">Ошибка загрузки.</string>
<string name="menu_bottom_navigation_change_url">Сменить URL</string> <string name="menu_bottom_navigation_change_url">Сменить URL</string>
<string name="searchable_recipe_main_hint">Найти рецепты</string>
</resources> </resources>

View File

@@ -52,4 +52,5 @@
<string name="fragment_recipes_load_failure_toast_unexpected_response">unexpected response</string> <string name="fragment_recipes_load_failure_toast_unexpected_response">unexpected response</string>
<string name="fragment_recipes_load_failure_toast_no_connection">no connection</string> <string name="fragment_recipes_load_failure_toast_no_connection">no connection</string>
<string name="menu_bottom_navigation_change_url">Change URL</string> <string name="menu_bottom_navigation_change_url">Change URL</string>
<string name="searchable_recipe_main_hint">Search recipes</string>
</resources> </resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/searchable_recipe_main_hint"
android:label="@string/app_name" />