From 1b5720f2a59b6c8abd639e30c62f1c7474c626e1 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 22 May 2025 09:05:25 +0300 Subject: [PATCH] Migrate to mdc search view --- .../koitharu/kotatsu/main/ui/MainActivity.kt | 296 ++++++------------ .../kotatsu/main/ui/MainMenuProvider.kt | 44 +++ .../koitharu/kotatsu/main/ui/MainViewModel.kt | 7 + .../search/domain/MangaSearchRepository.kt | 23 +- .../ui/suggestion/SearchSuggestionFragment.kt | 67 ---- .../ui/suggestion/SearchSuggestionListener.kt | 5 +- .../SearchSuggestionListenerImpl.kt | 57 ++++ .../suggestion/SearchSuggestionViewModel.kt | 56 ++-- .../suggestion/model/SearchSuggestionItem.kt | 4 +- .../search/ui/widget/SearchEditText.kt | 161 ---------- .../kotatsu/search/ui/widget/SearchToolbar.kt | 24 -- .../kotatsu/suggestions/data/SuggestionDao.kt | 4 + .../res/layout-w600dp-land/activity_main.xml | 60 ++-- app/src/main/res/layout/activity_main.xml | 63 ++-- 14 files changed, 291 insertions(+), 580 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainMenuProvider.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListenerImpl.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchToolbar.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt index 3f94c1009..00efeaefc 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -5,18 +5,13 @@ import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Build import android.os.Bundle -import android.view.Menu -import android.view.MenuItem import android.view.View import android.view.ViewGroup.MarginLayoutParams -import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels import androidx.appcompat.view.ActionMode import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import androidx.core.net.toUri import androidx.core.view.MenuProvider -import androidx.core.view.SoftwareKeyboardControllerCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.children import androidx.core.view.inputmethod.EditorInfoCompat @@ -25,32 +20,37 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentTransaction -import androidx.fragment.app.commit import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.withResumed -import androidx.transition.TransitionManager +import androidx.recyclerview.widget.ItemTouchHelper import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP +import com.google.android.material.search.SearchView import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.nav.router -import org.koitharu.kotatsu.core.parser.MangaLinkResolver +import org.koitharu.kotatsu.core.os.VoiceInputContract import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.NavItem import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.util.FadingAppbarMediator import org.koitharu.kotatsu.core.ui.util.MenuInvalidator -import org.koitharu.kotatsu.core.ui.util.OptionsMenuBadgeHelper import org.koitharu.kotatsu.core.ui.widgets.SlidingBottomNavigationView import org.koitharu.kotatsu.core.util.ext.consume import org.koitharu.kotatsu.core.util.ext.end @@ -66,35 +66,35 @@ import org.koitharu.kotatsu.local.ui.LocalStorageCleanupWorker import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.remotelist.ui.MangaSearchMenuProvider -import org.koitharu.kotatsu.search.domain.SearchKind -import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment -import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionItemCallback +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListenerImpl +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionMenuProvider import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel +import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter import org.koitharu.kotatsu.settings.backup.PeriodicalBackupService import javax.inject.Inject -import androidx.appcompat.R as appcompatR - -private const val TAG_SEARCH = "search" +import com.google.android.material.R as materialR @AndroidEntryPoint class MainActivity : BaseActivity(), AppBarOwner, BottomNavOwner, View.OnClickListener, - View.OnFocusChangeListener, - SearchSuggestionListener, + SearchSuggestionItemCallback.SuggestionItemListener, MainNavigationDelegate.OnFragmentChangedListener, - View.OnLayoutChangeListener { + View.OnLayoutChangeListener, + SearchView.TransitionListener { @Inject lateinit var settings: AppSettings private val viewModel by viewModels() private val searchSuggestionViewModel by viewModels() - private val closeSearchCallback = CloseSearchCallback() + private val voiceInputLauncher = registerForActivityResult(VoiceInputContract()) { result -> + if (result != null) { + viewBinding.searchView.setText(result) + } + } private lateinit var navigationDelegate: MainNavigationDelegate - private lateinit var appUpdateBadge: OptionsMenuBadgeHelper private lateinit var fadingAppbarMediator: FadingAppbarMediator override val appBar: AppBarLayout @@ -107,14 +107,10 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav super.onCreate(savedInstanceState) setContentView(ActivityMainBinding.inflate(layoutInflater)) - with(viewBinding.searchView) { - onFocusChangeListener = this@MainActivity - searchSuggestionListener = this@MainActivity - } - viewBinding.fab?.setOnClickListener(this) viewBinding.navRail?.headerView?.setOnClickListener(this) - fadingAppbarMediator = FadingAppbarMediator(viewBinding.appbar, viewBinding.toolbarCard) + fadingAppbarMediator = + FadingAppbarMediator(viewBinding.appbar, viewBinding.layoutSearch ?: viewBinding.searchBar) navigationDelegate = MainNavigationDelegate( navBar = checkNotNull(bottomNav ?: viewBinding.navRail), @@ -124,11 +120,10 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav navigationDelegate.addOnFragmentChangedListener(this) navigationDelegate.onCreate(this, savedInstanceState) - appUpdateBadge = OptionsMenuBadgeHelper(viewBinding.toolbar, R.id.action_app_update) + viewBinding.searchBar.addMenuProvider(MainMenuProvider(router, viewModel)) onBackPressedDispatcher.addCallback(ExitCallback(this, viewBinding.container)) onBackPressedDispatcher.addCallback(navigationDelegate) - onBackPressedDispatcher.addCallback(closeSearchCallback) if (savedInstanceState == null) { onFirstStart() @@ -139,16 +134,18 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav viewModel.isLoading.observe(this, this::onLoadingStateChanged) viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged) viewModel.feedCounter.observe(this, ::onFeedCounterChanged) - viewModel.appUpdate.observe(this, MenuInvalidator(this)) + viewModel.appUpdate.observe(this, MenuInvalidator(viewBinding.searchBar)) viewModel.onFirstStart.observeEvent(this) { router.showWelcomeSheet() } viewModel.isBottomNavPinned.observe(this, ::setNavbarPinned) searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged) viewBinding.bottomNav?.addOnLayoutChangeListener(this) + viewBinding.searchView.addTransitionListener(this) + initSearchSuggestions() } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) - adjustSearchUI(isSearchOpened(), animate = false) + adjustSearchUI(viewBinding.searchView.isShowing) navigationDelegate.syncSelectedItem() } @@ -157,7 +154,6 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav adjustAppbar(topFragment = fragment) if (fromUser) { actionModeDelegate.finishActionMode() - closeSearchCallback.handleOnBackPressed() viewBinding.appbar.setExpanded(true) } } @@ -168,51 +164,6 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav } } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - super.onCreateOptionsMenu(menu) - menuInflater.inflate(R.menu.opt_main, menu) - return true - } - - override fun onPrepareOptionsMenu(menu: Menu?): Boolean { - if (menu == null) { - return false - } - menu.findItem(R.id.action_incognito)?.isChecked = - searchSuggestionViewModel.isIncognitoModeEnabled.value - val hasAppUpdate = viewModel.appUpdate.value != null - menu.findItem(R.id.action_app_update)?.isVisible = hasAppUpdate - appUpdateBadge.setBadgeVisible(hasAppUpdate) - return super.onPrepareOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { - android.R.id.home -> if (isSearchOpened()) { - closeSearchCallback.handleOnBackPressed() - true - } else { - viewBinding.searchView.requestFocus() - true - } - - R.id.action_settings -> { - router.openSettings() - true - } - - R.id.action_incognito -> { - viewModel.setIncognitoMode(!item.isChecked) - true - } - - R.id.action_app_update -> { - router.openAppUpdate() - true - } - - else -> super.onOptionsItemSelected(item) - } - override fun onClick(v: View) { when (v.id) { R.id.fab, R.id.railFab -> viewModel.openLastReader() @@ -222,12 +173,13 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat { val typeMask = WindowInsetsCompat.Type.systemBars() val barsInsets = insets.getInsets(typeMask) - viewBinding.toolbarCard.updateLayoutParams { - marginEnd = barsInsets.end(v) + val searchBarDefaultMargin = resources.getDimensionPixelOffset(materialR.dimen.m3_searchbar_margin_horizontal) + viewBinding.searchBar.updateLayoutParams { + marginEnd = searchBarDefaultMargin + barsInsets.end(v) marginStart = if (viewBinding.navRail != null) { - 0 + searchBarDefaultMargin } else { - barsInsets.start(v) + searchBarDefaultMargin + barsInsets.start(v) } } viewBinding.bottomNav?.updatePadding( @@ -241,7 +193,9 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav bottomMargin = barsInsets.bottom } updateContainerBottomMargin() - return insets.consume(v, typeMask, start = viewBinding.navRail != null) + return insets.consume(v, typeMask, start = viewBinding.navRail != null).also { + handleSearchSuggestionsInsets(it) + } } override fun onLayoutChange( @@ -260,65 +214,27 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav } } - override fun onFocusChange(v: View?, hasFocus: Boolean) { - val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH) - if (v?.id == R.id.searchView && hasFocus) { - if (fragment == null) { - supportFragmentManager.commit { - setReorderingAllowed(true) - add(R.id.container, SearchSuggestionFragment(), TAG_SEARCH) - navigationDelegate.primaryFragment?.let { - setMaxLifecycle(it, Lifecycle.State.STARTED) - } - setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) - runOnCommit { onSearchOpened() } - } - } + override fun onStateChanged( + searchView: SearchView, + previousState: SearchView.TransitionState, + newState: SearchView.TransitionState, + ) { + val wasOpened = previousState >= SearchView.TransitionState.SHOWING + val isOpened = newState >= SearchView.TransitionState.SHOWING + if (isOpened != wasOpened) { + adjustSearchUI(isOpened) } } - override fun onMangaClick(manga: Manga) { - router.openDetails(manga) - } - - override fun onQueryClick(query: String, kind: SearchKind, submit: Boolean) { - viewBinding.searchView.query = query - if (submit && query.isNotEmpty()) { - if (kind == SearchKind.SIMPLE && MangaLinkResolver.isValidLink(query)) { - router.openDetails(query.toUri()) - } else { - router.openSearch(query, kind) - if (kind != SearchKind.TAG) { - searchSuggestionViewModel.saveQuery(query) - } - } - viewBinding.searchView.post { - closeSearchCallback.handleOnBackPressed() - } - } - } - - override fun onTagClick(tag: MangaTag) { - router.openSearch(tag.title, SearchKind.TAG) - } - - override fun onQueryChanged(query: String) { - searchSuggestionViewModel.onQueryChanged(query) - } - - override fun onSourceToggle(source: MangaSource, isEnabled: Boolean) { - searchSuggestionViewModel.onSourceToggle(source, isEnabled) - } - - override fun onSourceClick(source: MangaSource) { - router.openList(source, null, null) + override fun onRemoveQuery(query: String) { + searchSuggestionViewModel.deleteQuery(query) } override fun onSupportActionModeStarted(mode: ActionMode) { super.onSupportActionModeStarted(mode) adjustFabVisibility() bottomNav?.hide() - viewBinding.toolbarCard.isInvisible = true + (viewBinding.layoutSearch ?: viewBinding.searchBar).isInvisible = true updateContainerBottomMargin() } @@ -326,7 +242,7 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav super.onSupportActionModeFinished(mode) adjustFabVisibility() bottomNav?.show() - viewBinding.toolbarCard.isInvisible = false + (viewBinding.layoutSearch ?: viewBinding.searchBar).isInvisible = false updateContainerBottomMargin() } @@ -340,14 +256,14 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav } private fun onIncognitoModeChanged(isIncognito: Boolean) { - var options = viewBinding.searchView.imeOptions + var options = viewBinding.searchView.getEditText().imeOptions options = if (isIncognito) { options or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING } else { options and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv() } - viewBinding.searchView.imeOptions = options - invalidateMenu() + viewBinding.searchView.getEditText().imeOptions = options + viewBinding.searchBar.invalidateMenu() } private fun onLoadingStateChanged(isLoading: Boolean) { @@ -359,19 +275,6 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav adjustFabVisibility(isResumeEnabled = isEnabled) } - private fun onSearchOpened() { - adjustSearchUI(isOpened = true, animate = true) - } - - private fun onSearchClosed() { - SoftwareKeyboardControllerCompat(viewBinding.searchView).hide() - adjustSearchUI(isOpened = false, animate = true) - } - - private fun isSearchOpened(): Boolean { - return supportFragmentManager.findFragmentByTag(TAG_SEARCH) != null - } - private fun onFirstStart() { lifecycleScope.launch(Dispatchers.Main) { // not a default `Main.immediate` dispatcher withContext(Dispatchers.Default) { @@ -399,7 +302,7 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav private fun adjustFabVisibility( isResumeEnabled: Boolean = viewModel.isResumeEnabled.value, topFragment: Fragment? = navigationDelegate.primaryFragment, - isSearchOpened: Boolean = isSearchOpened(), + isSearchOpened: Boolean = viewBinding.searchView.isShowing, ) { val fab = viewBinding.fab ?: return if (isResumeEnabled && !actionModeDelegate.isActionModeStarted && !isSearchOpened && topFragment is HistoryListFragment) { @@ -413,46 +316,17 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav } } - private fun adjustSearchUI(isOpened: Boolean, animate: Boolean) { - if (animate) { - TransitionManager.beginDelayedTransition(viewBinding.appbar) - } + private fun adjustSearchUI(isOpened: Boolean) { val appBarScrollFlags = if (isOpened) { SCROLL_FLAG_NO_SCROLL } else { SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS or SCROLL_FLAG_SNAP } - viewBinding.toolbarCard.updateLayoutParams { - scrollFlags = appBarScrollFlags - } viewBinding.insetsHolder.updateLayoutParams { scrollFlags = appBarScrollFlags } - viewBinding.toolbarCard.background = if (isOpened) { - null - } else { - ContextCompat.getDrawable(this, R.drawable.search_bar_background) - } - val padding = if (isOpened) 0 else resources.getDimensionPixelOffset(R.dimen.margin_normal) - viewBinding.appbar.updatePadding(left = padding, right = padding) adjustFabVisibility(isSearchOpened = isOpened) - supportActionBar?.apply { - setHomeAsUpIndicator( - if (isOpened) { - appcompatR.drawable.abc_ic_ab_back_material - } else { - appcompatR.drawable.abc_ic_search_api_material - }, - ) - setHomeActionContentDescription( - if (isOpened) R.string.back else R.string.search, - ) - } - viewBinding.searchView.setHintCompat( - if (isOpened) R.string.search_hint else R.string.search_manga, - ) bottomNav?.showOrHide(!isOpened) - closeSearchCallback.isEnabled = isOpened updateContainerBottomMargin() } @@ -470,6 +344,39 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav } } + private fun handleSearchSuggestionsInsets(insets: WindowInsetsCompat) { + val typeMask = WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.systemBars() + val barsInsets = insets.getInsets(typeMask) + viewBinding.recyclerViewSearch.setPadding(barsInsets.left, 0, barsInsets.right, barsInsets.bottom) + } + + private fun initSearchSuggestions() { + val listener = SearchSuggestionListenerImpl(router, viewBinding.searchView, searchSuggestionViewModel) + val adapter = SearchSuggestionAdapter(listener) + viewBinding.searchView.toolbar.addMenuProvider( + SearchSuggestionMenuProvider(this, voiceInputLauncher, searchSuggestionViewModel), + ) + viewBinding.searchView.editText.addTextChangedListener(listener) + viewBinding.recyclerViewSearch.adapter = adapter + + viewBinding.searchView.observeState() + .map { it >= SearchView.TransitionState.SHOWING } + .distinctUntilChanged() + .flatMapLatest { isShowing -> + if (isShowing) { + searchSuggestionViewModel.suggestion + } else { + emptyFlow() + } + }.observe(this, adapter) + searchSuggestionViewModel.onError.observeEvent( + this, + SnackbarErrorObserver(viewBinding.recyclerViewSearch, null), + ) + ItemTouchHelper(SearchSuggestionItemCallback(this)) + .attachToRecyclerView(viewBinding.recyclerViewSearch) + } + private fun setNavbarPinned(isPinned: Boolean) { val bottomNavBar = viewBinding.bottomNav bottomNavBar?.isPinned = isPinned @@ -500,26 +407,11 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav } } - private inner class CloseSearchCallback : OnBackPressedCallback(false) { - - override fun handleOnBackPressed() { - val fm = supportFragmentManager - val fragment = fm.findFragmentByTag(TAG_SEARCH) - viewBinding.searchView.clearFocus() - if (fragment == null) { - // this should not happen but who knows - isEnabled = false - return - } - fm.commit { - setReorderingAllowed(true) - remove(fragment) - navigationDelegate.primaryFragment?.let { - setMaxLifecycle(it, Lifecycle.State.RESUMED) - } - setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) - runOnCommit { onSearchClosed() } - } + private fun SearchView.observeState() = callbackFlow { + val listener = SearchView.TransitionListener { _, _, state -> + trySendBlocking(state) } + addTransitionListener(listener) + awaitClose { removeTransitionListener(listener) } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainMenuProvider.kt new file mode 100644 index 000000000..4274a53d2 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainMenuProvider.kt @@ -0,0 +1,44 @@ +package org.koitharu.kotatsu.main.ui + +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.nav.AppRouter + +class MainMenuProvider( + private val router: AppRouter, + private val viewModel: MainViewModel, +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_main, menu) + } + + override fun onPrepareMenu(menu: Menu) { + menu.findItem(R.id.action_incognito)?.isChecked = + viewModel.isIncognitoModeEnabled.value + val hasAppUpdate = viewModel.appUpdate.value != null + menu.findItem(R.id.action_app_update)?.isVisible = hasAppUpdate + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_settings -> { + router.openSettings() + true + } + + R.id.action_incognito -> { + viewModel.setIncognitoMode(!menuItem.isChecked) + true + } + + R.id.action_app_update -> { + router.openAppUpdate() + true + } + + else -> false + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainViewModel.kt index 88e3a7ed0..5f2360c15 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainViewModel.kt @@ -11,6 +11,7 @@ import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.github.AppUpdateRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.observeAsFlow +import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.call @@ -54,6 +55,12 @@ class MainViewModel @Inject constructor( isNavBarPinned }.flowOn(Dispatchers.Default) + val isIncognitoModeEnabled = settings.observeAsStateFlow( + scope = viewModelScope + Dispatchers.Default, + key = AppSettings.KEY_INCOGNITO_MODE, + valueProducer = { isIncognitoModeEnabled }, + ) + init { launchJob { appUpdateRepository.fetchUpdate() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index 75ed86995..a427b809a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.koitharu.kotatsu.core.db.MangaDatabase -import org.koitharu.kotatsu.core.db.entity.MangaWithTags import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toMangaTag @@ -35,18 +34,16 @@ class MangaSearchRepository @Inject constructor( private val settings: AppSettings, ) { - suspend fun getMangaSuggestion(query: String, limit: Int, source: MangaSource?): List { - return when { - query.isEmpty() -> db.getSuggestionDao().getRandom(limit).map { MangaWithTags(it.manga, emptyList()) } - source != null -> db.getMangaDao().searchByTitle("%$query%", source.name, limit) - else -> db.getMangaDao().searchByTitle("%$query%", limit) - }.let { - if (settings.isNsfwContentDisabled) it.filterNot { x -> x.manga.isNsfw } else it - }.map { - it.toManga() - }.sortedBy { x -> - x.title.levenshteinDistance(query) - } + suspend fun getMangaSuggestion(query: String, limit: Int, source: MangaSource?): List = when { + query.isEmpty() -> db.getSuggestionDao().getTopManga(limit) + source != null -> db.getMangaDao().searchByTitle("%$query%", source.name, limit) + else -> db.getMangaDao().searchByTitle("%$query%", limit) + }.let { + if (settings.isNsfwContentDisabled) it.filterNot { x -> x.manga.isNsfw } else it + }.map { + it.toManga() + }.sortedBy { x -> + x.title.levenshteinDistance(query) } suspend fun getQuerySuggestion( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt deleted file mode 100644 index 40488eb9e..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -package org.koitharu.kotatsu.search.ui.suggestion - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.WindowInsetsCompat -import androidx.fragment.app.activityViewModels -import androidx.recyclerview.widget.ItemTouchHelper -import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver -import org.koitharu.kotatsu.core.os.VoiceInputContract -import org.koitharu.kotatsu.core.ui.BaseFragment -import org.koitharu.kotatsu.core.util.ext.addMenuProvider -import org.koitharu.kotatsu.core.util.ext.consumeAll -import org.koitharu.kotatsu.core.util.ext.observe -import org.koitharu.kotatsu.core.util.ext.observeEvent -import org.koitharu.kotatsu.databinding.FragmentSearchSuggestionBinding -import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter - -@AndroidEntryPoint -class SearchSuggestionFragment : - BaseFragment(), - SearchSuggestionItemCallback.SuggestionItemListener { - - private val viewModel by activityViewModels() - private val voiceInputLauncher = registerForActivityResult(VoiceInputContract()) { result -> - if (result != null) { - viewModel.onQueryChanged(result) - } - } - - override fun onCreateViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - ) = FragmentSearchSuggestionBinding.inflate(inflater, container, false) - - override fun onViewBindingCreated(binding: FragmentSearchSuggestionBinding, savedInstanceState: Bundle?) { - super.onViewBindingCreated(binding, savedInstanceState) - val adapter = SearchSuggestionAdapter( - listener = requireActivity() as SearchSuggestionListener, - ) - addMenuProvider(SearchSuggestionMenuProvider(binding.root.context, voiceInputLauncher, viewModel)) - binding.root.adapter = adapter - binding.root.setHasFixedSize(true) - viewModel.suggestion.observe(viewLifecycleOwner, adapter) - viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.root, this)) - ItemTouchHelper(SearchSuggestionItemCallback(this)) - .attachToRecyclerView(binding.root) - } - - override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat { - val typeMask = WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.systemBars() - val barsInsets = insets.getInsets(typeMask) - v.setPadding(barsInsets.left, 0, barsInsets.right, barsInsets.bottom) - return insets.consumeAll(typeMask) - } - - override fun onRemoveQuery(query: String) { - viewModel.deleteQuery(query) - } - - override fun onResume() { - super.onResume() - viewModel.onResume() - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt index 7a89cac70..a76382d9c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt @@ -1,18 +1,17 @@ package org.koitharu.kotatsu.search.ui.suggestion +import android.text.TextWatcher import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.search.domain.SearchKind -interface SearchSuggestionListener { +interface SearchSuggestionListener : TextWatcher { fun onMangaClick(manga: Manga) fun onQueryClick(query: String, kind: SearchKind, submit: Boolean) - fun onQueryChanged(query: String) - fun onSourceToggle(source: MangaSource, isEnabled: Boolean) fun onSourceClick(source: MangaSource) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListenerImpl.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListenerImpl.kt new file mode 100644 index 000000000..74d97bee5 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListenerImpl.kt @@ -0,0 +1,57 @@ +package org.koitharu.kotatsu.search.ui.suggestion + +import android.text.Editable +import androidx.core.net.toUri +import com.google.android.material.search.SearchView +import org.koitharu.kotatsu.core.nav.AppRouter +import org.koitharu.kotatsu.core.parser.MangaLinkResolver +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.search.domain.SearchKind + +class SearchSuggestionListenerImpl( + private val router: AppRouter, + private val searchView: SearchView, + private val viewModel: SearchSuggestionViewModel, +) : SearchSuggestionListener { + + override fun onMangaClick(manga: Manga) { + router.openDetails(manga) + } + + override fun onQueryClick(query: String, kind: SearchKind, submit: Boolean) { + if (submit && query.isNotEmpty()) { + if (kind == SearchKind.SIMPLE && MangaLinkResolver.isValidLink(query)) { + router.openDetails(query.toUri()) + } else { + router.openSearch(query, kind) + if (kind != SearchKind.TAG) { + viewModel.saveQuery(query) + } + } + } else { + searchView.setText(query) + } + } + + override fun onTagClick(tag: MangaTag) { + router.openSearch(tag.title, SearchKind.TAG) + } + + override fun onSourceToggle(source: MangaSource, isEnabled: Boolean) { + viewModel.onSourceToggle(source, isEnabled) + } + + override fun onSourceClick(source: MangaSource) { + router.openList(source, null, null) + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit + + override fun afterTextChanged(s: Editable?) { + viewModel.onQueryChanged(s?.toString().orEmpty()) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt index d4c779413..a0b8063fb 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt @@ -3,17 +3,16 @@ package org.koitharu.kotatsu.search.ui.suggestion import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.plus import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.SearchSuggestionType @@ -48,8 +47,7 @@ class SearchSuggestionViewModel @Inject constructor( ) : BaseViewModel() { private val query = MutableStateFlow("") - private var suggestionJob: Job? = null - private var invalidateOnResume = false + private val invalidationTrigger = MutableStateFlow(0) val isIncognitoModeEnabled = settings.observeAsStateFlow( scope = viewModelScope + Dispatchers.Default, @@ -57,11 +55,19 @@ class SearchSuggestionViewModel @Inject constructor( valueProducer = { isIncognitoModeEnabled }, ) - val suggestion = MutableStateFlow>(emptyList()) - - init { - setupSuggestion() - } + val suggestion: Flow> = combine( + query.debounce(DEBOUNCE_TIMEOUT), + sourcesRepository.observeEnabledSources().map { it.mapToSet { x -> x.name } }, + settings.observeAsFlow(AppSettings.KEY_SEARCH_SUGGESTION_TYPES) { searchSuggestionTypes }, + invalidationTrigger, + ) + { a, b, c, _ -> + Triple(a, b, c) + }.mapLatest { (searchQuery, enabledSources, types) -> + buildSearchSuggestion(searchQuery, enabledSources, types) + }.distinctUntilChanged() + .withErrorHandling() + .flowOn(Dispatchers.Default) fun onQueryChanged(newQuery: String) { query.value = newQuery @@ -70,14 +76,14 @@ class SearchSuggestionViewModel @Inject constructor( fun saveQuery(query: String) { if (!settings.isIncognitoModeEnabled) { repository.saveSearchQuery(query) + invalidationTrigger.value++ } - invalidateOnResume = true } fun clearSearchHistory() { launchJob(Dispatchers.Default) { repository.clearSearchHistory() - setupSuggestion() + invalidationTrigger.value++ } } @@ -87,35 +93,13 @@ class SearchSuggestionViewModel @Inject constructor( } } - fun onResume() { - if (invalidateOnResume || suggestionJob?.isActive != true) { - invalidateOnResume = false - setupSuggestion() - } - } - fun deleteQuery(query: String) { launchJob(Dispatchers.Default) { repository.deleteSearchQuery(query) - setupSuggestion() + invalidationTrigger.value++ } } - private fun setupSuggestion() { - suggestionJob?.cancel() - suggestionJob = combine( - query.debounce(DEBOUNCE_TIMEOUT), - sourcesRepository.observeEnabledSources().map { it.mapToSet { x -> x.name } }, - settings.observeAsFlow(AppSettings.KEY_SEARCH_SUGGESTION_TYPES) { searchSuggestionTypes }, - ::Triple, - ).mapLatest { (searchQuery, enabledSources, types) -> - buildSearchSuggestion(searchQuery, enabledSources, types) - }.distinctUntilChanged() - .onEach { - suggestion.value = it - }.withErrorHandling().launchIn(viewModelScope + Dispatchers.Default) - } - private suspend fun buildSearchSuggestion( searchQuery: String, enabledSources: Set, @@ -210,7 +194,7 @@ class SearchSuggestionViewModel @Inject constructor( listOf(SearchSuggestionItem.Text(0, e)) } - private suspend fun getSources(searchQuery: String, enabledSources: Set): List = + private fun getSources(searchQuery: String, enabledSources: Set): List = runCatchingCancellable { repository.getSourcesSuggestion(searchQuery, MAX_SOURCES_ITEMS) .map { SearchSuggestionItem.Source(it, it.name in enabledSources) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt index a0a428f1a..09ab4411c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt @@ -55,7 +55,7 @@ sealed interface SearchSuggestionItem : ListModel { get() = source.isNsfw() override fun areItemsTheSame(other: ListModel): Boolean { - return other is Source && other.source == source + return other is Source && other.source.name == source.name } override fun getChangePayload(previousState: ListModel): Any? { @@ -78,7 +78,7 @@ sealed interface SearchSuggestionItem : ListModel { get() = source.isNsfw() override fun areItemsTheSame(other: ListModel): Boolean { - return other is Source && other.source == source + return other is SourceTip && other.source.name == source.name } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt deleted file mode 100644 index 4a34becdd..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt +++ /dev/null @@ -1,161 +0,0 @@ -package org.koitharu.kotatsu.search.ui.widget - -import android.annotation.SuppressLint -import android.content.Context -import android.os.Parcelable -import android.text.Spannable -import android.text.SpannableString -import android.text.style.TextAppearanceSpan -import android.util.AttributeSet -import android.view.InputDevice -import android.view.KeyEvent -import android.view.MotionEvent -import android.view.SoundEffectConstants -import android.view.accessibility.AccessibilityEvent -import android.view.inputmethod.EditorInfo -import androidx.annotation.AttrRes -import androidx.annotation.CheckResult -import androidx.annotation.StringRes -import androidx.appcompat.widget.AppCompatEditText -import androidx.core.content.ContextCompat -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.util.ext.drawableEnd -import org.koitharu.kotatsu.core.util.ext.drawableStart -import org.koitharu.kotatsu.search.domain.SearchKind -import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener -import androidx.appcompat.R as appcompatR - -private const val DRAWABLE_END = 2 - -class SearchEditText @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - @AttrRes defStyleAttr: Int = appcompatR.attr.editTextStyle, -) : AppCompatEditText(context, attrs, defStyleAttr) { - - var searchSuggestionListener: SearchSuggestionListener? = null - private val clearIcon = - ContextCompat.getDrawable(context, appcompatR.drawable.abc_ic_clear_material) - private var isEmpty = text.isNullOrEmpty() - - init { - hint = wrapHint() - } - - var query: String - get() = text?.trim()?.toString().orEmpty() - set(value) { - if (value != text?.toString()) { - setText(value) - setSelection(value.length) - } - } - - override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean { - if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { - if (hasFocus()) { - clearFocus() - } - } - return super.onKeyPreIme(keyCode, event) - } - - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { - if (event.isFromSource(InputDevice.SOURCE_KEYBOARD) - && (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER) - && event.hasNoModifiers() - && query.isNotEmpty() - ) { - cancelLongPress() - searchSuggestionListener?.onQueryClick(query, SearchKind.SIMPLE, submit = true) - clearFocus() - return true - } - return super.onKeyUp(keyCode, event) - } - - override fun onEditorAction(actionCode: Int) { - super.onEditorAction(actionCode) - if (actionCode == EditorInfo.IME_ACTION_SEARCH) { - searchSuggestionListener?.onQueryClick(query, SearchKind.SIMPLE, submit = true) - } - } - - override fun onTextChanged( - text: CharSequence?, - start: Int, - lengthBefore: Int, - lengthAfter: Int, - ) { - super.onTextChanged(text, start, lengthBefore, lengthAfter) - val empty = text.isNullOrEmpty() - if (isEmpty != empty) { - isEmpty = empty - updateActionIcon() - } - searchSuggestionListener?.onQueryChanged(query) - } - - override fun onRestoreInstanceState(state: Parcelable?) { - super.onRestoreInstanceState(state) - updateActionIcon() - } - - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(event: MotionEvent): Boolean { - if (event.action == MotionEvent.ACTION_UP) { - val drawable = - compoundDrawablesRelative[DRAWABLE_END] ?: return super.onTouchEvent(event) - val isOnDrawable = drawable.isVisible && if (layoutDirection == LAYOUT_DIRECTION_RTL) { - event.x.toInt() in paddingLeft..(drawable.bounds.width() + paddingLeft) - } else { - event.x.toInt() in (width - drawable.bounds.width() - paddingRight)..(width - paddingRight) - } - if (isOnDrawable) { - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED) - playSoundEffect(SoundEffectConstants.CLICK) - onActionIconClick() - return true - } - } - return super.onTouchEvent(event) - } - - override fun clearFocus() { - super.clearFocus() - text?.clear() - } - - fun setHintCompat(@StringRes resId: Int) { - hint = wrapHint(context.getString(resId)) - } - - private fun onActionIconClick() { - when { - !text.isNullOrEmpty() -> text?.clear() - } - } - - private fun updateActionIcon() { - val icon = when { - !text.isNullOrEmpty() -> clearIcon - else -> null - } - if (icon !== drawableEnd) { - setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, null, icon, null) - } - } - - @CheckResult - private fun wrapHint(raw: CharSequence? = hint): SpannableString? { - val rawHint = raw?.toString() ?: return null - val formatted = SpannableString(rawHint) - formatted.setSpan( - TextAppearanceSpan(context, R.style.TextAppearance_Kotatsu_SearchView), - 0, - formatted.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, - ) - return formatted - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchToolbar.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchToolbar.kt deleted file mode 100644 index e45040610..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchToolbar.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.koitharu.kotatsu.search.ui.widget - -import android.content.Context -import android.graphics.Color -import android.util.AttributeSet -import androidx.annotation.AttrRes -import com.google.android.material.appbar.MaterialToolbar -import com.google.android.material.shape.MaterialShapeDrawable -import androidx.appcompat.R as appcompatR - -class SearchToolbar @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - @AttrRes defStyleAttr: Int = appcompatR.attr.toolbarStyle, -) : MaterialToolbar(context, attrs, defStyleAttr) { - - private val bgDrawable = MaterialShapeDrawable(context, attrs, defStyleAttr, 0) - - init { - bgDrawable.initializeElevationOverlay(context) - bgDrawable.setShadowColor(Color.DKGRAY) - background = bgDrawable - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt index 12e0eb5b3..f94536517 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt @@ -33,6 +33,10 @@ abstract class SuggestionDao : MangaQueryBuilder.ConditionCallback { .build(), ) + @Transaction + @Query("SELECT manga.* FROM suggestions LEFT JOIN manga ON manga.manga_id = suggestions.manga_id ORDER BY relevance DESC LIMIT :limit") + abstract suspend fun getTopManga(limit: Int): List + @Transaction open suspend fun getRandom(limit: Int): List { val ids = getRandomIds(limit) diff --git a/app/src/main/res/layout-w600dp-land/activity_main.xml b/app/src/main/res/layout-w600dp-land/activity_main.xml index 209d41c16..aa56128d6 100644 --- a/app/src/main/res/layout-w600dp-land/activity_main.xml +++ b/app/src/main/res/layout-w600dp-land/activity_main.xml @@ -19,7 +19,8 @@ app:layout_constraintStart_toStartOf="parent" app:paddingBottomSystemWindowInsets="false" app:paddingStartSystemWindowInsets="false" - app:paddingTopSystemWindowInsets="false" /> + app:paddingTopSystemWindowInsets="false" + app:scrollingEnabled="true" /> - - - - - + android:layout_gravity="center_vertical|end" + android:hint="@string/search_manga" + app:adaptiveMaxWidthEnabled="true" /> + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d5c96b518..e32153886 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -22,7 +22,6 @@ android:clipToPadding="false" android:elevation="0dp" android:fitsSystemWindows="false" - android:paddingHorizontal="@dimen/margin_normal" android:stateListAnimator="@null" app:elevation="0dp" app:liftOnScroll="false" @@ -36,49 +35,33 @@ android:fitsSystemWindows="true" app:layout_scrollFlags="scroll|enterAlways|snap" /> - - - - - - - - - + android:layout_height="wrap_content" + android:hint="@string/search_manga" + app:layout_scrollFlags="scroll|enterAlways|snap" /> + + + + + +