mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-15 05:27:28 +01:00
Move more components to presentation-core module
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
package eu.kanade.presentation.track
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.DropdownMenu
|
||||
import eu.kanade.presentation.track.components.TrackLogoIcon
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import tachiyomi.presentation.core.components.material.Divider
|
||||
import tachiyomi.presentation.core.components.material.VerticalDivider
|
||||
import java.text.DateFormat
|
||||
|
||||
private const val UnsetStatusTextAlpha = 0.5F
|
||||
|
||||
@Composable
|
||||
fun TrackInfoDialogHome(
|
||||
trackItems: List<TrackItem>,
|
||||
dateFormat: DateFormat,
|
||||
onStatusClick: (TrackItem) -> Unit,
|
||||
onChapterClick: (TrackItem) -> Unit,
|
||||
onScoreClick: (TrackItem) -> Unit,
|
||||
onStartDateEdit: (TrackItem) -> Unit,
|
||||
onEndDateEdit: (TrackItem) -> Unit,
|
||||
onNewSearch: (TrackItem) -> Unit,
|
||||
onOpenInBrowser: (TrackItem) -> Unit,
|
||||
onRemoved: (TrackItem) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.animateContentSize()
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp)
|
||||
.windowInsetsPadding(WindowInsets.systemBars),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
trackItems.forEach { item ->
|
||||
if (item.track != null) {
|
||||
val supportsScoring = item.service.getScoreList().isNotEmpty()
|
||||
val supportsReadingDates = item.service.supportsReadingDates
|
||||
TrackInfoItem(
|
||||
title = item.track.title,
|
||||
service = item.service,
|
||||
status = item.service.getStatus(item.track.status),
|
||||
onStatusClick = { onStatusClick(item) },
|
||||
chapters = "${item.track.last_chapter_read.toInt()}".let {
|
||||
val totalChapters = item.track.total_chapters
|
||||
if (totalChapters > 0) {
|
||||
// Add known total chapter count
|
||||
"$it / $totalChapters"
|
||||
} else {
|
||||
it
|
||||
}
|
||||
},
|
||||
onChaptersClick = { onChapterClick(item) },
|
||||
score = item.service.displayScore(item.track)
|
||||
.takeIf { supportsScoring && item.track.score != 0F },
|
||||
onScoreClick = { onScoreClick(item) }
|
||||
.takeIf { supportsScoring },
|
||||
startDate = remember(item.track.started_reading_date) { dateFormat.format(item.track.started_reading_date) }
|
||||
.takeIf { supportsReadingDates && item.track.started_reading_date != 0L },
|
||||
onStartDateClick = { onStartDateEdit(item) } // TODO
|
||||
.takeIf { supportsReadingDates },
|
||||
endDate = dateFormat.format(item.track.finished_reading_date)
|
||||
.takeIf { supportsReadingDates && item.track.finished_reading_date != 0L },
|
||||
onEndDateClick = { onEndDateEdit(item) }
|
||||
.takeIf { supportsReadingDates },
|
||||
onNewSearch = { onNewSearch(item) },
|
||||
onOpenInBrowser = { onOpenInBrowser(item) },
|
||||
onRemoved = { onRemoved(item) },
|
||||
)
|
||||
} else {
|
||||
TrackInfoItemEmpty(
|
||||
service = item.service,
|
||||
onNewSearch = { onNewSearch(item) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackInfoItem(
|
||||
title: String,
|
||||
service: TrackService,
|
||||
status: String,
|
||||
onStatusClick: () -> Unit,
|
||||
chapters: String,
|
||||
onChaptersClick: () -> Unit,
|
||||
score: String?,
|
||||
onScoreClick: (() -> Unit)?,
|
||||
startDate: String?,
|
||||
onStartDateClick: (() -> Unit)?,
|
||||
endDate: String?,
|
||||
onEndDateClick: (() -> Unit)?,
|
||||
onNewSearch: () -> Unit,
|
||||
onOpenInBrowser: () -> Unit,
|
||||
onRemoved: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TrackLogoIcon(
|
||||
service = service,
|
||||
onClick = onOpenInBrowser,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.weight(1f)
|
||||
.combinedClickable(
|
||||
onClick = onNewSearch,
|
||||
onLongClick = {
|
||||
context.copyToClipboard(title, title)
|
||||
},
|
||||
)
|
||||
.padding(start = 16.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
VerticalDivider()
|
||||
TrackInfoItemMenu(
|
||||
onOpenInBrowser = onOpenInBrowser,
|
||||
onRemoved = onRemoved,
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 12.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(8.dp)
|
||||
.clip(RoundedCornerShape(6.dp)),
|
||||
) {
|
||||
Column {
|
||||
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||
TrackDetailsItem(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = status,
|
||||
onClick = onStatusClick,
|
||||
)
|
||||
VerticalDivider()
|
||||
TrackDetailsItem(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = chapters,
|
||||
onClick = onChaptersClick,
|
||||
)
|
||||
if (onScoreClick != null) {
|
||||
VerticalDivider()
|
||||
TrackDetailsItem(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.alpha(if (score == null) UnsetStatusTextAlpha else 1f),
|
||||
text = score ?: stringResource(R.string.score),
|
||||
onClick = onScoreClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (onStartDateClick != null && onEndDateClick != null) {
|
||||
Divider()
|
||||
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||
TrackDetailsItem(
|
||||
modifier = Modifier.weight(1F),
|
||||
text = startDate,
|
||||
placeholder = stringResource(R.string.track_started_reading_date),
|
||||
onClick = onStartDateClick,
|
||||
)
|
||||
VerticalDivider()
|
||||
TrackDetailsItem(
|
||||
modifier = Modifier.weight(1F),
|
||||
text = endDate,
|
||||
placeholder = stringResource(R.string.track_finished_reading_date),
|
||||
onClick = onEndDateClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackDetailsItem(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String?,
|
||||
placeholder: String = "",
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clickable(onClick = onClick)
|
||||
.alpha(if (text == null) UnsetStatusTextAlpha else 1f)
|
||||
.fillMaxHeight()
|
||||
.padding(12.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = text ?: placeholder,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackInfoItemEmpty(
|
||||
service: TrackService,
|
||||
onNewSearch: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TrackLogoIcon(service)
|
||||
TextButton(
|
||||
onClick = onNewSearch,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
.weight(1f),
|
||||
) {
|
||||
Text(text = stringResource(R.string.add_tracking))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackInfoItemMenu(
|
||||
onOpenInBrowser: () -> Unit,
|
||||
onRemoved: () -> Unit,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
|
||||
IconButton(onClick = { expanded = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.label_more),
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.action_open_in_browser)) },
|
||||
onClick = {
|
||||
onOpenInBrowser()
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.action_remove)) },
|
||||
onClick = {
|
||||
onRemoved()
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package eu.kanade.presentation.track
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
import tachiyomi.presentation.core.components.WheelDatePicker
|
||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||
import tachiyomi.presentation.core.components.material.AlertDialogContent
|
||||
import tachiyomi.presentation.core.components.material.Divider
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||
import tachiyomi.presentation.core.util.isScrolledToStart
|
||||
import tachiyomi.presentation.core.util.minimumTouchTargetSize
|
||||
import java.time.LocalDate
|
||||
import java.time.format.TextStyle
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun TrackStatusSelector(
|
||||
selection: Int,
|
||||
onSelectionChange: (Int) -> Unit,
|
||||
selections: Map<Int, String>,
|
||||
onConfirm: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
BaseSelector(
|
||||
title = stringResource(R.string.status),
|
||||
content = {
|
||||
val state = rememberLazyListState()
|
||||
ScrollbarLazyColumn(state = state) {
|
||||
selections.forEach { (key, value) ->
|
||||
val isSelected = selection == key
|
||||
item {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.selectable(
|
||||
selected = isSelected,
|
||||
onClick = { onSelectionChange(key) },
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.minimumTouchTargetSize(),
|
||||
) {
|
||||
RadioButton(
|
||||
selected = isSelected,
|
||||
onClick = null,
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodyLarge.merge(),
|
||||
modifier = Modifier.padding(start = 24.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!state.isScrolledToStart()) Divider(modifier = Modifier.align(Alignment.TopCenter))
|
||||
if (!state.isScrolledToEnd()) Divider(modifier = Modifier.align(Alignment.BottomCenter))
|
||||
},
|
||||
onConfirm = onConfirm,
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TrackChapterSelector(
|
||||
selection: Int,
|
||||
onSelectionChange: (Int) -> Unit,
|
||||
range: Iterable<Int>,
|
||||
onConfirm: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
BaseSelector(
|
||||
title = stringResource(R.string.chapters),
|
||||
content = {
|
||||
WheelTextPicker(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
startIndex = selection,
|
||||
texts = range.map { "$it" },
|
||||
onSelectionChanged = { onSelectionChange(it) },
|
||||
)
|
||||
},
|
||||
onConfirm = onConfirm,
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TrackScoreSelector(
|
||||
selection: String,
|
||||
onSelectionChange: (String) -> Unit,
|
||||
selections: List<String>,
|
||||
onConfirm: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
BaseSelector(
|
||||
title = stringResource(R.string.score),
|
||||
content = {
|
||||
WheelTextPicker(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
startIndex = selections.indexOf(selection).coerceAtLeast(0),
|
||||
texts = selections,
|
||||
onSelectionChanged = { onSelectionChange(selections[it]) },
|
||||
)
|
||||
},
|
||||
onConfirm = onConfirm,
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TrackDateSelector(
|
||||
title: String,
|
||||
minDate: LocalDate?,
|
||||
maxDate: LocalDate?,
|
||||
selection: LocalDate,
|
||||
onSelectionChange: (LocalDate) -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
onRemove: (() -> Unit)?,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
BaseSelector(
|
||||
title = title,
|
||||
content = {
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
var internalSelection by remember { mutableStateOf(selection) }
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 16.dp),
|
||||
text = internalSelection.dayOfWeek
|
||||
.getDisplayName(TextStyle.SHORT, Locale.getDefault()),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
WheelDatePicker(
|
||||
startDate = selection,
|
||||
minDate = minDate,
|
||||
maxDate = maxDate,
|
||||
onSelectionChanged = {
|
||||
internalSelection = it
|
||||
onSelectionChange(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
thirdButton = if (onRemove != null) {
|
||||
{
|
||||
TextButton(onClick = onRemove) {
|
||||
Text(text = stringResource(R.string.action_remove))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onConfirm = onConfirm,
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BaseSelector(
|
||||
title: String,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
thirdButton: @Composable (RowScope.() -> Unit)? = null,
|
||||
onConfirm: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
AlertDialogContent(
|
||||
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars),
|
||||
title = { Text(text = title) },
|
||||
text = {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
content = content,
|
||||
)
|
||||
},
|
||||
buttons = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small, Alignment.End),
|
||||
) {
|
||||
if (thirdButton != null) {
|
||||
thirdButton()
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
}
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
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.filled.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.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.res.stringResource
|
||||
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.unit.dp
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.manga.components.MangaCover
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import tachiyomi.presentation.core.components.LoadingScreen
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Divider
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
|
||||
@Composable
|
||||
fun TrackServiceSearch(
|
||||
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.Default.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 = stringResource(R.string.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
},
|
||||
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 = stringResource(R.string.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),
|
||||
textResource = R.string.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
|
||||
?: stringResource(R.string.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
|
||||
.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.Default.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 = stringResource(R.string.track_type),
|
||||
text = type,
|
||||
)
|
||||
}
|
||||
if (startDate.isNotBlank()) {
|
||||
SearchResultItemDetails(
|
||||
title = stringResource(R.string.label_started),
|
||||
text = startDate,
|
||||
)
|
||||
}
|
||||
if (status.isNotBlank()) {
|
||||
SearchResultItemDetails(
|
||||
title = stringResource(R.string.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package eu.kanade.presentation.track.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||
|
||||
@Composable
|
||||
fun TrackLogoIcon(
|
||||
service: TrackService,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val modifier = if (onClick != null) {
|
||||
Modifier.clickableNoIndication(onClick = onClick)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(48.dp)
|
||||
.background(color = Color(service.getLogoColor()), shape = MaterialTheme.shapes.medium)
|
||||
.padding(4.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(service.getLogo()),
|
||||
contentDescription = stringResource(service.nameRes()),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user