Convert settings main and search views to full Compose

This commit is contained in:
arkon 2022-08-29 16:39:35 -04:00
parent 761635b572
commit f5c7aa1142
12 changed files with 148 additions and 294 deletions

View file

@ -1,13 +1,18 @@
package eu.kanade.presentation.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -18,18 +23,23 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.delay
@Composable
fun AppBar(
@ -206,6 +216,51 @@ fun AppBarActions(
}
}
@Composable
fun SearchToolbar(
searchQuery: String,
onChangeSearchQuery: (String) -> Unit,
onClickCloseSearch: () -> Unit,
onClickResetSearch: () -> Unit,
incognitoMode: Boolean = false,
downloadedOnlyMode: Boolean = false,
scrollBehavior: TopAppBarScrollBehavior? = null,
) {
val focusRequester = remember { FocusRequester.Default }
AppBar(
titleContent = {
BasicTextField(
value = searchQuery,
onValueChange = onChangeSearchQuery,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onBackground),
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
)
},
navigationIcon = Icons.Outlined.ArrowBack,
navigateUp = onClickCloseSearch,
actions = {
AnimatedVisibility(visible = searchQuery.isNotEmpty()) {
IconButton(onClick = onClickResetSearch) {
Icon(Icons.Outlined.Close, contentDescription = stringResource(R.string.action_reset))
}
}
},
isActionMode = false,
downloadedOnlyMode = downloadedOnlyMode,
incognitoMode = incognitoMode,
scrollBehavior = scrollBehavior,
)
LaunchedEffect(focusRequester) {
// TODO: https://issuetracker.google.com/issues/204502668
delay(100)
focusRequester.requestFocus()
}
}
sealed interface AppBar {
sealed interface AppBarAction

View file

@ -1,30 +1,18 @@
package eu.kanade.presentation.history.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.DeleteSweep
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter
import eu.kanade.tachiyomi.ui.recent.history.HistoryState
import kotlinx.coroutines.delay
@Composable
fun HistoryToolbar(
@ -42,7 +30,7 @@ fun HistoryToolbar(
scrollBehavior = scrollBehavior,
)
} else {
HistorySearchToolbar(
SearchToolbar(
searchQuery = state.searchQuery!!,
onChangeSearchQuery = { state.searchQuery = it },
onClickCloseSearch = { state.searchQuery = null },
@ -76,46 +64,3 @@ fun HistoryRegularToolbar(
scrollBehavior = scrollBehavior,
)
}
@Composable
fun HistorySearchToolbar(
searchQuery: String,
onChangeSearchQuery: (String) -> Unit,
onClickCloseSearch: () -> Unit,
onClickResetSearch: () -> Unit,
incognitoMode: Boolean,
downloadedOnlyMode: Boolean,
) {
val focusRequester = remember { FocusRequester.Default }
AppBar(
titleContent = {
BasicTextField(
value = searchQuery,
onValueChange = onChangeSearchQuery,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onBackground),
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
)
},
navigationIcon = Icons.Outlined.ArrowBack,
navigateUp = onClickCloseSearch,
actions = {
AnimatedVisibility(visible = searchQuery.isNotEmpty()) {
IconButton(onClick = onClickResetSearch) {
Icon(Icons.Outlined.Close, contentDescription = stringResource(R.string.action_reset))
}
}
},
isActionMode = false,
downloadedOnlyMode = downloadedOnlyMode,
incognitoMode = incognitoMode,
)
LaunchedEffect(focusRequester) {
// TODO: https://issuetracker.google.com/issues/204502668
delay(100)
focusRequester.requestFocus()
}
}

View file

@ -1,12 +1,8 @@
package eu.kanade.presentation.library.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.Refresh
@ -19,22 +15,17 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.Pill
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.library.LibraryState
import eu.kanade.presentation.theme.active
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.delay
@Composable
fun LibraryToolbar(
@ -57,14 +48,14 @@ fun LibraryToolbar(
onClickSelectAll = onClickSelectAll,
onClickInvertSelection = onClickInvertSelection,
)
state.searchQuery != null -> LibrarySearchToolbar(
state.searchQuery != null -> SearchToolbar(
searchQuery = state.searchQuery!!,
incognitoMode = incognitoMode,
downloadedOnlyMode = downloadedOnlyMode,
onChangeSearchQuery = { state.searchQuery = it },
onClickCloseSearch = { state.searchQuery = null },
onClickResetSearch = { state.searchQuery = "" },
scrollBehavior = scrollBehavior,
incognitoMode = incognitoMode,
downloadedOnlyMode = downloadedOnlyMode,
)
else -> LibraryRegularToolbar(
title = title,
@ -152,49 +143,6 @@ fun LibrarySelectionToolbar(
)
}
@Composable
fun LibrarySearchToolbar(
searchQuery: String,
incognitoMode: Boolean,
downloadedOnlyMode: Boolean,
onChangeSearchQuery: (String) -> Unit,
onClickCloseSearch: () -> Unit,
onClickResetSearch: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior?,
) {
val focusRequester = remember { FocusRequester.Default }
AppBar(
navigateUp = onClickCloseSearch,
titleContent = {
BasicTextField(
value = searchQuery,
onValueChange = onChangeSearchQuery,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onBackground),
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
)
LaunchedEffect(focusRequester) {
// TODO: https://issuetracker.google.com/issues/204502668
delay(100)
focusRequester.requestFocus()
}
},
actions = {
AnimatedVisibility(visible = searchQuery.isNotEmpty()) {
IconButton(onClick = onClickResetSearch) {
Icon(Icons.Outlined.Close, contentDescription = stringResource(R.string.action_reset))
}
}
},
incognitoMode = incognitoMode,
downloadedOnlyMode = downloadedOnlyMode,
scrollBehavior = scrollBehavior,
)
}
data class LibraryToolbarTitle(
val text: String,
val numberOfManga: Int? = null,

View file

@ -4,31 +4,58 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Search
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R
@Composable
fun SettingsMainScreen(
nestedScrollInterop: NestedScrollConnection,
navigateUp: () -> Unit,
sections: List<SettingsSection>,
onClickSearch: () -> Unit,
) {
ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) {
sections.map {
item {
PreferenceRow(
title = stringResource(it.titleRes),
painter = it.painter,
onClick = it.onClick,
)
Scaffold(
modifier = Modifier.statusBarsPadding(),
topBar = {
AppBar(
title = stringResource(R.string.label_settings),
navigateUp = navigateUp,
actions = {
AppBarActions(
listOf(
AppBar.Action(
title = stringResource(R.string.action_search),
icon = Icons.Outlined.Search,
onClick = onClickSearch,
),
),
)
},
)
},
) { paddingValues ->
ScrollbarLazyColumn(
contentPadding = paddingValues + WindowInsets.navigationBars.asPaddingValues(),
) {
sections.map {
item {
PreferenceRow(
title = stringResource(it.titleRes),
painter = it.painter,
onClick = it.onClick,
)
}
}
}
}

View file

@ -7,19 +7,23 @@ import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.ui.setting.search.SettingsSearchHelper
import eu.kanade.tachiyomi.ui.setting.search.SettingsSearchPresenter
@ -27,24 +31,39 @@ import kotlin.reflect.full.createInstance
@Composable
fun SettingsSearchScreen(
nestedScroll: NestedScrollConnection,
navigateUp: () -> Unit,
presenter: SettingsSearchPresenter,
onClickResult: (SettingsController) -> Unit,
) {
val results by presenter.state.collectAsState()
var query by remember { mutableStateOf("") }
val scrollState = rememberLazyListState()
ScrollbarLazyColumn(
modifier = Modifier
.nestedScroll(nestedScroll),
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
state = scrollState,
) {
items(
items = results,
key = { it.key.toString() },
) { result ->
SearchResult(result, onClickResult)
Scaffold(
modifier = Modifier.statusBarsPadding(),
topBar = {
SearchToolbar(
searchQuery = query,
onChangeSearchQuery = {
query = it
presenter.searchSettings(it)
},
onClickCloseSearch = navigateUp,
onClickResetSearch = { query = "" },
)
// TODO: search placeholder
// Text(stringResource(R.string.action_search_settings))
},
) { paddingValues ->
ScrollbarLazyColumn(
contentPadding = paddingValues + WindowInsets.navigationBars.asPaddingValues(),
) {
items(
items = results,
key = { it.key.toString() },
) { result ->
SearchResult(result, onClickResult)
}
}
}
}

View file

@ -299,8 +299,6 @@ class PreferencesHelper(val context: Context) {
fun defaultUserAgent() = flowPrefs.getString(Keys.defaultUserAgent, "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0")
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
fun filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, DomainManga.SHOW_ALL.toInt())
fun filterChapterByDownloaded() = prefs.getInt(Keys.defaultChapterFilterByDownloaded, DomainManga.SHOW_ALL.toInt())

View file

@ -51,28 +51,6 @@ abstract class ComposeController<P : Presenter<*>>(bundle: Bundle? = null) :
}
}
/**
* Basic Compose controller without a presenter.
*/
abstract class BasicComposeController :
BaseController<ComposeControllerBinding>(),
ComposeContentController {
override fun createBinding(inflater: LayoutInflater) =
ComposeControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.root.apply {
setComposeContent {
val nestedScrollInterop = rememberNestedScrollInteropConnection()
ComposeContent(nestedScrollInterop)
}
}
}
}
/**
* Basic Compose controller without a presenter.
*/

View file

@ -37,7 +37,7 @@ class ExtensionDetailsPresenter(
presenterScope.launchIO {
extensionManager.getInstalledExtensionsFlow()
.map { it.firstOrNull { pkg-> pkg.pkgName == pkgName } }
.map { it.firstOrNull { pkg -> pkg.pkgName == pkgName } }
.collectLatest { extension ->
// If extension is null it's most likely uninstalled
if (extension == null) {

View file

@ -1,9 +1,5 @@
package eu.kanade.tachiyomi.ui.setting
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ChromeReaderMode
import androidx.compose.material.icons.outlined.Code
@ -15,25 +11,18 @@ import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.res.painterResource
import eu.kanade.presentation.more.settings.SettingsMainScreen
import eu.kanade.presentation.more.settings.SettingsSection
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.controller.BasicComposeController
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.setting.search.SettingsSearchController
import uy.kohesive.injekt.injectLazy
class SettingsMainController : BasicComposeController() {
private val preferences: PreferencesHelper by injectLazy()
override fun getTitle() = resources?.getString(R.string.label_settings)
class SettingsMainController : BasicFullComposeController() {
@Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
override fun ComposeContent() {
val settingsSections = listOf(
SettingsSection(
titleRes = R.string.pref_category_general,
@ -88,34 +77,9 @@ class SettingsMainController : BasicComposeController() {
)
SettingsMainScreen(
nestedScrollInterop = nestedScrollInterop,
navigateUp = router::popCurrentController,
sections = settingsSections,
)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.settings_main, menu)
// Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
// Change hint to show global search.
searchView.queryHint = applicationContext?.getString(R.string.action_search_settings)
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
preferences.lastSearchQuerySearchSettings().set("") // reset saved search query
router.pushController(SettingsSearchController())
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
return true
}
},
onClickSearch = { router.pushController(SettingsSearchController()) },
)
}
}

View file

@ -1,80 +1,20 @@
package eu.kanade.tachiyomi.ui.setting.search
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import eu.kanade.presentation.more.settings.SettingsSearchScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
class SettingsSearchController : ComposeController<SettingsSearchPresenter>() {
private lateinit var searchView: SearchView
init {
setHasOptionsMenu(true)
}
override fun getTitle() = presenter.query
class SettingsSearchController : FullComposeController<SettingsSearchPresenter>() {
override fun createPresenter() = SettingsSearchPresenter()
@Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
override fun ComposeContent() {
SettingsSearchScreen(
nestedScroll = nestedScrollInterop,
navigateUp = router::popCurrentController,
presenter = presenter,
onClickResult = { controller ->
searchView.query.let {
presenter.setLastSearchQuerySearchSettings(it.toString())
}
router.pushController(controller)
},
onClickResult = { router.pushController(it) },
)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.settings_main, menu)
// Initialize search menu
val searchItem = menu.findItem(R.id.action_search)
searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
searchView.queryHint = applicationContext?.getString(R.string.action_search_settings)
searchItem.expandActionView()
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
router.popCurrentController()
return false
}
},
)
searchView.setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
presenter.searchSettings(query)
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
presenter.searchSettings(newText)
return false
}
},
)
searchView.setQuery(presenter.getLastSearchQuerySearchSettings(), true)
}
}

View file

@ -23,14 +23,6 @@ class SettingsSearchPresenter(
SettingsSearchHelper.initPreferenceSearchResults(preferences.context)
}
fun getLastSearchQuerySearchSettings(): String {
return preferences.lastSearchQuerySearchSettings().get()
}
fun setLastSearchQuerySearchSettings(query: String) {
preferences.lastSearchQuerySearchSettings().set(query)
}
fun searchSettings(query: String?) {
_state.value = if (!query.isNullOrBlank()) {
SettingsSearchHelper.getFilteredResults(query)

View file

@ -1,12 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search_24dp"
android:title="@string/action_search"
app:actionViewClass="eu.kanade.tachiyomi.widget.TachiyomiSearchView"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="collapseActionView|ifRoom" />
</menu>