123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- package eu.kanade.presentation.track
- import androidx.compose.animation.AnimatedVisibility
- import androidx.compose.animation.fadeIn
- import androidx.compose.animation.fadeOut
- import androidx.compose.animation.slideInVertically
- import androidx.compose.animation.slideOutVertically
- import androidx.compose.foundation.background
- import androidx.compose.foundation.border
- import androidx.compose.foundation.layout.Arrangement
- import androidx.compose.foundation.layout.Box
- import androidx.compose.foundation.layout.Column
- import androidx.compose.foundation.layout.PaddingValues
- import androidx.compose.foundation.layout.Row
- import androidx.compose.foundation.layout.Spacer
- import androidx.compose.foundation.layout.WindowInsets
- import androidx.compose.foundation.layout.fillMaxWidth
- import androidx.compose.foundation.layout.height
- import androidx.compose.foundation.layout.navigationBars
- import androidx.compose.foundation.layout.padding
- import androidx.compose.foundation.layout.paddingFromBaseline
- import androidx.compose.foundation.layout.width
- import androidx.compose.foundation.layout.windowInsetsPadding
- import androidx.compose.foundation.lazy.items
- import androidx.compose.foundation.selection.selectable
- import androidx.compose.foundation.shape.RoundedCornerShape
- import androidx.compose.foundation.text.BasicTextField
- import androidx.compose.foundation.text.KeyboardActions
- import androidx.compose.foundation.text.KeyboardOptions
- import androidx.compose.material.icons.Icons
- import androidx.compose.material.icons.automirrored.outlined.ArrowBack
- import androidx.compose.material.icons.filled.CheckCircle
- import androidx.compose.material.icons.filled.Close
- import androidx.compose.material3.Button
- import androidx.compose.material3.ButtonDefaults
- import androidx.compose.material3.HorizontalDivider
- import androidx.compose.material3.Icon
- import androidx.compose.material3.IconButton
- import androidx.compose.material3.MaterialTheme
- import androidx.compose.material3.Text
- import androidx.compose.material3.TopAppBar
- import androidx.compose.runtime.Composable
- import androidx.compose.runtime.remember
- import androidx.compose.ui.Alignment
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.draw.clip
- import androidx.compose.ui.focus.FocusRequester
- import androidx.compose.ui.focus.focusRequester
- import androidx.compose.ui.graphics.Color
- import androidx.compose.ui.graphics.SolidColor
- import androidx.compose.ui.platform.LocalFocusManager
- import androidx.compose.ui.text.capitalize
- import androidx.compose.ui.text.input.ImeAction
- import androidx.compose.ui.text.input.TextFieldValue
- import androidx.compose.ui.text.intl.Locale
- import androidx.compose.ui.text.style.TextOverflow
- import androidx.compose.ui.text.toLowerCase
- import androidx.compose.ui.tooling.preview.PreviewLightDark
- import androidx.compose.ui.tooling.preview.PreviewParameter
- import androidx.compose.ui.unit.dp
- import eu.kanade.presentation.manga.components.MangaCover
- import eu.kanade.presentation.theme.TachiyomiTheme
- import eu.kanade.tachiyomi.data.track.model.TrackSearch
- import tachiyomi.i18n.MR
- import tachiyomi.presentation.core.components.ScrollbarLazyColumn
- import tachiyomi.presentation.core.components.material.Scaffold
- import tachiyomi.presentation.core.components.material.padding
- import tachiyomi.presentation.core.i18n.localize
- import tachiyomi.presentation.core.screens.EmptyScreen
- import tachiyomi.presentation.core.screens.LoadingScreen
- import tachiyomi.presentation.core.util.plus
- import tachiyomi.presentation.core.util.runOnEnterKeyPressed
- import tachiyomi.presentation.core.util.secondaryItemAlpha
- @Composable
- fun TrackerSearch(
- query: TextFieldValue,
- onQueryChange: (TextFieldValue) -> Unit,
- onDispatchQuery: () -> Unit,
- queryResult: Result<List<TrackSearch>>?,
- selected: TrackSearch?,
- onSelectedChange: (TrackSearch) -> Unit,
- onConfirmSelection: () -> Unit,
- onDismissRequest: () -> Unit,
- ) {
- val focusManager = LocalFocusManager.current
- val focusRequester = remember { FocusRequester() }
- val dispatchQueryAndClearFocus: () -> Unit = {
- onDispatchQuery()
- focusManager.clearFocus()
- }
- Scaffold(
- topBar = {
- Column {
- TopAppBar(
- navigationIcon = {
- IconButton(onClick = onDismissRequest) {
- Icon(
- imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
- contentDescription = null,
- tint = MaterialTheme.colorScheme.onSurfaceVariant,
- )
- }
- },
- title = {
- BasicTextField(
- value = query,
- onValueChange = onQueryChange,
- modifier = Modifier
- .fillMaxWidth()
- .focusRequester(focusRequester)
- .runOnEnterKeyPressed(action = dispatchQueryAndClearFocus),
- textStyle = MaterialTheme.typography.bodyLarge
- .copy(color = MaterialTheme.colorScheme.onSurface),
- singleLine = true,
- keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
- keyboardActions = KeyboardActions(onSearch = { dispatchQueryAndClearFocus() }),
- cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
- decorationBox = {
- if (query.text.isEmpty()) {
- Text(
- text = localize(MR.strings.action_search_hint),
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- style = MaterialTheme.typography.bodyLarge,
- )
- }
- it()
- },
- )
- },
- actions = {
- if (query.text.isNotEmpty()) {
- IconButton(
- onClick = {
- onQueryChange(TextFieldValue())
- focusRequester.requestFocus()
- },
- ) {
- Icon(
- imageVector = Icons.Default.Close,
- contentDescription = null,
- tint = MaterialTheme.colorScheme.onSurfaceVariant,
- )
- }
- }
- },
- )
- HorizontalDivider()
- }
- },
- bottomBar = {
- AnimatedVisibility(
- visible = selected != null,
- enter = fadeIn() + slideInVertically { it / 2 },
- exit = slideOutVertically { it / 2 } + fadeOut(),
- ) {
- Button(
- onClick = { onConfirmSelection() },
- modifier = Modifier
- .padding(12.dp)
- .windowInsetsPadding(WindowInsets.navigationBars)
- .fillMaxWidth(),
- elevation = ButtonDefaults.elevatedButtonElevation(),
- ) {
- Text(text = localize(MR.strings.action_track))
- }
- }
- },
- ) { innerPadding ->
- if (queryResult == null) {
- LoadingScreen(modifier = Modifier.padding(innerPadding))
- } else {
- val availableTracks = queryResult.getOrNull()
- if (availableTracks != null) {
- if (availableTracks.isEmpty()) {
- EmptyScreen(
- modifier = Modifier.padding(innerPadding),
- stringRes = MR.strings.no_results_found,
- )
- } else {
- ScrollbarLazyColumn(
- contentPadding = innerPadding + PaddingValues(vertical = 12.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- items(
- items = availableTracks,
- key = { it.hashCode() },
- ) {
- SearchResultItem(
- title = it.title,
- coverUrl = it.cover_url,
- type = it.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current),
- startDate = it.start_date,
- status = it.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current),
- description = it.summary.trim(),
- selected = it == selected,
- onClick = { onSelectedChange(it) },
- )
- }
- }
- }
- } else {
- EmptyScreen(
- modifier = Modifier.padding(innerPadding),
- message = queryResult.exceptionOrNull()?.message
- ?: localize(MR.strings.unknown_error),
- )
- }
- }
- }
- }
- @Composable
- private fun SearchResultItem(
- title: String,
- coverUrl: String,
- type: String,
- startDate: String,
- status: String,
- description: String,
- selected: Boolean,
- onClick: () -> Unit,
- ) {
- val shape = RoundedCornerShape(16.dp)
- val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 12.dp)
- .clip(shape)
- .background(MaterialTheme.colorScheme.surface)
- .border(
- width = 2.dp,
- color = borderColor,
- shape = shape,
- )
- .selectable(selected = selected, onClick = onClick)
- .padding(12.dp),
- ) {
- if (selected) {
- Icon(
- imageVector = Icons.Filled.CheckCircle,
- contentDescription = null,
- modifier = Modifier.align(Alignment.TopEnd),
- tint = MaterialTheme.colorScheme.primary,
- )
- }
- Column {
- Row {
- MangaCover.Book(
- data = coverUrl,
- modifier = Modifier.height(96.dp),
- )
- Spacer(modifier = Modifier.width(12.dp))
- Column {
- Text(
- text = title,
- modifier = Modifier.padding(end = 28.dp),
- maxLines = 2,
- overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.titleMedium,
- )
- if (type.isNotBlank()) {
- SearchResultItemDetails(
- title = localize(MR.strings.track_type),
- text = type,
- )
- }
- if (startDate.isNotBlank()) {
- SearchResultItemDetails(
- title = localize(MR.strings.label_started),
- text = startDate,
- )
- }
- if (status.isNotBlank()) {
- SearchResultItemDetails(
- title = localize(MR.strings.track_status),
- text = status,
- )
- }
- }
- }
- if (description.isNotBlank()) {
- Text(
- text = description,
- modifier = Modifier
- .paddingFromBaseline(top = 24.dp)
- .secondaryItemAlpha(),
- maxLines = 4,
- overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.bodySmall,
- )
- }
- }
- }
- }
- @Composable
- private fun SearchResultItemDetails(
- title: String,
- text: String,
- ) {
- Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) {
- Text(
- text = title,
- maxLines = 1,
- style = MaterialTheme.typography.titleSmall,
- )
- Text(
- text = text,
- modifier = Modifier
- .weight(1f)
- .secondaryItemAlpha(),
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.bodyMedium,
- )
- }
- }
- @PreviewLightDark
- @Composable
- private fun TrackerSearchPreviews(
- @PreviewParameter(TrackerSearchPreviewProvider::class)
- content: @Composable () -> Unit,
- ) {
- TachiyomiTheme { content() }
- }
|