Move more components to presentation-core module

This commit is contained in:
arkon
2023-02-18 16:33:03 -05:00
parent bfe143015a
commit 58a0add4f6
92 changed files with 176 additions and 175 deletions

View File

@@ -48,18 +48,14 @@ import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap
import eu.kanade.domain.manga.model.chaptersFiltered
import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.MangaBottomActionMenu
import eu.kanade.presentation.components.VerticalFastScroller
import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.ChapterHeader
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
import eu.kanade.presentation.manga.components.MangaActionRow
import eu.kanade.presentation.manga.components.MangaBottomActionMenu
import eu.kanade.presentation.manga.components.MangaChapterListItem
import eu.kanade.presentation.manga.components.MangaInfoBox
import eu.kanade.presentation.manga.components.MangaToolbar
import eu.kanade.presentation.util.isScrolledToEnd
import eu.kanade.presentation.util.isScrollingUp
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.SourceManager
@@ -71,10 +67,14 @@ import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.LazyColumn
import tachiyomi.presentation.core.components.TwoPanelBox
import tachiyomi.presentation.core.components.VerticalFastScroller
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.PullRefresh
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp
import java.text.DateFormat
import java.util.Date

View File

@@ -1,312 +0,0 @@
package eu.kanade.presentation.manga
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.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
},
)
}
}
}

View File

@@ -1,227 +0,0 @@
package eu.kanade.presentation.manga
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.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.components.WheelDatePicker
import eu.kanade.presentation.components.WheelTextPicker
import eu.kanade.presentation.util.isScrolledToEnd
import eu.kanade.presentation.util.isScrolledToStart
import eu.kanade.presentation.util.minimumTouchTargetSize
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.Divider
import tachiyomi.presentation.core.components.material.padding
import java.time.LocalDate
import java.time.format.TextStyle
@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, java.util.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))
}
}
},
)
}

View File

@@ -1,316 +0,0 @@
package eu.kanade.presentation.manga
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.components.MangaCover
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.runOnEnterKeyPressed
import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import tachiyomi.presentation.core.components.LoadingScreen
import tachiyomi.presentation.core.components.material.Divider
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
@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,
)
}
}

View File

@@ -14,7 +14,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.MangaCover
import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.material.padding

View File

@@ -0,0 +1,274 @@
package eu.kanade.presentation.manga.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text
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.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import tachiyomi.presentation.core.components.material.IconButtonTokens
import tachiyomi.presentation.core.util.secondaryItemAlpha
enum class ChapterDownloadAction {
START,
START_NOW,
CANCEL,
DELETE,
}
@Composable
fun ChapterDownloadIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
downloadStateProvider: () -> Download.State,
downloadProgressProvider: () -> Int,
onClick: (ChapterDownloadAction) -> Unit,
) {
when (val downloadState = downloadStateProvider()) {
Download.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
enabled = enabled,
modifier = modifier,
onClick = onClick,
)
Download.State.QUEUE, Download.State.DOWNLOADING -> DownloadingIndicator(
enabled = enabled,
modifier = modifier,
downloadState = downloadState,
downloadProgressProvider = downloadProgressProvider,
onClick = onClick,
)
Download.State.DOWNLOADED -> DownloadedIndicator(
enabled = enabled,
modifier = modifier,
onClick = onClick,
)
Download.State.ERROR -> ErrorIndicator(
enabled = enabled,
modifier = modifier,
onClick = onClick,
)
}
}
@Composable
private fun NotDownloadedIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
onLongClick = { onClick(ChapterDownloadAction.START_NOW) },
onClick = { onClick(ChapterDownloadAction.START) },
)
.secondaryItemAlpha(),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(R.drawable.ic_download_chapter_24dp),
contentDescription = stringResource(R.string.manga_download),
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@Composable
private fun DownloadingIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
downloadState: Download.State,
downloadProgressProvider: () -> Int,
onClick: (ChapterDownloadAction) -> Unit,
) {
var isMenuExpanded by remember { mutableStateOf(false) }
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
onLongClick = { onClick(ChapterDownloadAction.CANCEL) },
onClick = { isMenuExpanded = true },
),
contentAlignment = Alignment.Center,
) {
val arrowColor: Color
val strokeColor = MaterialTheme.colorScheme.onSurfaceVariant
val downloadProgress = downloadProgressProvider()
val indeterminate = downloadState == Download.State.QUEUE ||
(downloadState == Download.State.DOWNLOADING && downloadProgress == 0)
if (indeterminate) {
arrowColor = strokeColor
CircularProgressIndicator(
modifier = IndicatorModifier,
color = strokeColor,
strokeWidth = IndicatorStrokeWidth,
)
} else {
val animatedProgress by animateFloatAsState(
targetValue = downloadProgress / 100f,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
)
arrowColor = if (animatedProgress < 0.5f) {
strokeColor
} else {
MaterialTheme.colorScheme.background
}
CircularProgressIndicator(
progress = animatedProgress,
modifier = IndicatorModifier,
color = strokeColor,
strokeWidth = IndicatorSize / 2,
)
}
DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_start_downloading_now)) },
onClick = {
onClick(ChapterDownloadAction.START_NOW)
isMenuExpanded = false
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_cancel)) },
onClick = {
onClick(ChapterDownloadAction.CANCEL)
isMenuExpanded = false
},
)
}
Icon(
imageVector = Icons.Outlined.ArrowDownward,
contentDescription = null,
modifier = ArrowModifier,
tint = arrowColor,
)
}
}
@Composable
private fun DownloadedIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
var isMenuExpanded by remember { mutableStateOf(false) }
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
onLongClick = { isMenuExpanded = true },
onClick = { isMenuExpanded = true },
),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Filled.CheckCircle,
contentDescription = null,
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_delete)) },
onClick = {
onClick(ChapterDownloadAction.DELETE)
isMenuExpanded = false
},
)
}
}
}
@Composable
private fun ErrorIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
onLongClick = { onClick(ChapterDownloadAction.START) },
onClick = { onClick(ChapterDownloadAction.START) },
),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Outlined.ErrorOutline,
contentDescription = stringResource(R.string.chapter_error),
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.error,
)
}
}
private fun Modifier.commonClickable(
enabled: Boolean,
onLongClick: () -> Unit,
onClick: () -> Unit,
) = composed {
val haptic = LocalHapticFeedback.current
this.combinedClickable(
enabled = enabled,
onLongClick = {
onLongClick()
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
onClick = onClick,
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
bounded = false,
radius = IconButtonTokens.StateLayerSize / 2,
),
)
}
private val IndicatorSize = 26.dp
private val IndicatorPadding = 2.dp
// To match composable parameter name when used later
private val IndicatorStrokeWidth = IndicatorPadding
private val IndicatorModifier = Modifier
.size(IndicatorSize)
.padding(IndicatorPadding)
private val ArrowModifier = Modifier
.size(IndicatorSize - 7.dp)

View File

@@ -0,0 +1,308 @@
package eu.kanade.presentation.manga.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.BookmarkAdd
import androidx.compose.material.icons.outlined.BookmarkRemove
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Label
import androidx.compose.material.icons.outlined.RemoveDone
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.DownloadDropdownMenu
import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
@Composable
fun MangaBottomActionMenu(
visible: Boolean,
modifier: Modifier = Modifier,
onBookmarkClicked: (() -> Unit)? = null,
onRemoveBookmarkClicked: (() -> Unit)? = null,
onMarkAsReadClicked: (() -> Unit)? = null,
onMarkAsUnreadClicked: (() -> Unit)? = null,
onMarkPreviousAsReadClicked: (() -> Unit)? = null,
onDownloadClicked: (() -> Unit)? = null,
onDeleteClicked: (() -> Unit)? = null,
) {
AnimatedVisibility(
visible = visible,
enter = expandVertically(expandFrom = Alignment.Bottom),
exit = shrinkVertically(shrinkTowards = Alignment.Bottom),
) {
val scope = rememberCoroutineScope()
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
tonalElevation = 3.dp,
) {
val haptic = LocalHapticFeedback.current
val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
var resetJob: Job? = remember { null }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0 until 7).forEach { i -> confirm[i] = i == toConfirmIndex }
resetJob?.cancel()
resetJob = scope.launch {
delay(1.seconds)
if (isActive) confirm[toConfirmIndex] = false
}
}
Row(
modifier = Modifier
.padding(
WindowInsets.navigationBars
.only(WindowInsetsSides.Bottom)
.asPaddingValues(),
)
.padding(horizontal = 8.dp, vertical = 12.dp),
) {
if (onBookmarkClicked != null) {
Button(
title = stringResource(R.string.action_bookmark),
icon = Icons.Outlined.BookmarkAdd,
toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) },
onClick = onBookmarkClicked,
)
}
if (onRemoveBookmarkClicked != null) {
Button(
title = stringResource(R.string.action_remove_bookmark),
icon = Icons.Outlined.BookmarkRemove,
toConfirm = confirm[1],
onLongClick = { onLongClickItem(1) },
onClick = onRemoveBookmarkClicked,
)
}
if (onMarkAsReadClicked != null) {
Button(
title = stringResource(R.string.action_mark_as_read),
icon = Icons.Outlined.DoneAll,
toConfirm = confirm[2],
onLongClick = { onLongClickItem(2) },
onClick = onMarkAsReadClicked,
)
}
if (onMarkAsUnreadClicked != null) {
Button(
title = stringResource(R.string.action_mark_as_unread),
icon = Icons.Outlined.RemoveDone,
toConfirm = confirm[3],
onLongClick = { onLongClickItem(3) },
onClick = onMarkAsUnreadClicked,
)
}
if (onMarkPreviousAsReadClicked != null) {
Button(
title = stringResource(R.string.action_mark_previous_as_read),
icon = ImageVector.vectorResource(id = R.drawable.ic_done_prev_24dp),
toConfirm = confirm[4],
onLongClick = { onLongClickItem(4) },
onClick = onMarkPreviousAsReadClicked,
)
}
if (onDownloadClicked != null) {
Button(
title = stringResource(R.string.action_download),
icon = Icons.Outlined.Download,
toConfirm = confirm[5],
onLongClick = { onLongClickItem(5) },
onClick = onDownloadClicked,
)
}
if (onDeleteClicked != null) {
Button(
title = stringResource(R.string.action_delete),
icon = Icons.Outlined.Delete,
toConfirm = confirm[6],
onLongClick = { onLongClickItem(6) },
onClick = onDeleteClicked,
)
}
}
}
}
}
@Composable
private fun RowScope.Button(
title: String,
icon: ImageVector,
toConfirm: Boolean,
onLongClick: () -> Unit,
onClick: () -> Unit,
content: (@Composable () -> Unit)? = null,
) {
val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
Column(
modifier = Modifier
.size(48.dp)
.weight(animatedWeight)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
onLongClick = onLongClick,
onClick = onClick,
),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = icon,
contentDescription = title,
)
AnimatedVisibility(
visible = toConfirm,
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
) {
Text(
text = title,
overflow = TextOverflow.Visible,
maxLines = 1,
style = MaterialTheme.typography.labelSmall,
)
}
content?.invoke()
}
}
@Composable
fun LibraryBottomActionMenu(
visible: Boolean,
modifier: Modifier = Modifier,
onChangeCategoryClicked: () -> Unit,
onMarkAsReadClicked: () -> Unit,
onMarkAsUnreadClicked: () -> Unit,
onDownloadClicked: ((DownloadAction) -> Unit)?,
onDeleteClicked: () -> Unit,
) {
AnimatedVisibility(
visible = visible,
enter = expandVertically(animationSpec = tween(delayMillis = 300)),
exit = shrinkVertically(animationSpec = tween()),
) {
val scope = rememberCoroutineScope()
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
tonalElevation = 3.dp,
) {
val haptic = LocalHapticFeedback.current
val confirm = remember { mutableStateListOf(false, false, false, false, false) }
var resetJob: Job? = remember { null }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0 until 5).forEach { i -> confirm[i] = i == toConfirmIndex }
resetJob?.cancel()
resetJob = scope.launch {
delay(1.seconds)
if (isActive) confirm[toConfirmIndex] = false
}
}
Row(
modifier = Modifier
.windowInsetsPadding(
WindowInsets.navigationBars
.only(WindowInsetsSides.Bottom),
)
.padding(horizontal = 8.dp, vertical = 12.dp),
) {
Button(
title = stringResource(R.string.action_move_category),
icon = Icons.Outlined.Label,
toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) },
onClick = onChangeCategoryClicked,
)
Button(
title = stringResource(R.string.action_mark_as_read),
icon = Icons.Outlined.DoneAll,
toConfirm = confirm[1],
onLongClick = { onLongClickItem(1) },
onClick = onMarkAsReadClicked,
)
Button(
title = stringResource(R.string.action_mark_as_unread),
icon = Icons.Outlined.RemoveDone,
toConfirm = confirm[2],
onLongClick = { onLongClickItem(2) },
onClick = onMarkAsUnreadClicked,
)
if (onDownloadClicked != null) {
var downloadExpanded by remember { mutableStateOf(false) }
Button(
title = stringResource(R.string.action_download),
icon = Icons.Outlined.Download,
toConfirm = confirm[3],
onLongClick = { onLongClickItem(3) },
onClick = { downloadExpanded = !downloadExpanded },
) {
val onDismissRequest = { downloadExpanded = false }
DownloadDropdownMenu(
expanded = downloadExpanded,
onDismissRequest = onDismissRequest,
onDownloadClicked = onDownloadClicked,
includeDownloadAllOption = false,
)
}
}
Button(
title = stringResource(R.string.action_delete),
icon = Icons.Outlined.Delete,
toConfirm = confirm[4],
onLongClick = { onLongClickItem(4) },
onClick = onDeleteClicked,
)
}
}
}
}

View File

@@ -27,13 +27,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.ChapterDownloadIndicator
import eu.kanade.presentation.util.selectedBackground
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import tachiyomi.presentation.core.components.material.ReadItemAlpha
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
import tachiyomi.presentation.core.util.selectedBackground
@Composable
fun MangaChapterListItem(

View File

@@ -0,0 +1,54 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.semantics.Role
import coil.compose.AsyncImage
import eu.kanade.presentation.util.rememberResourceBitmapPainter
import eu.kanade.tachiyomi.R
enum class MangaCover(val ratio: Float) {
Square(1f / 1f),
Book(2f / 3f),
;
@Composable
operator fun invoke(
modifier: Modifier = Modifier,
data: Any?,
contentDescription: String = "",
shape: Shape = MaterialTheme.shapes.extraSmall,
onClick: (() -> Unit)? = null,
) {
AsyncImage(
model = data,
placeholder = ColorPainter(CoverPlaceholderColor),
error = rememberResourceBitmapPainter(id = R.drawable.cover_error),
contentDescription = contentDescription,
modifier = modifier
.aspectRatio(ratio)
.clip(shape)
.then(
if (onClick != null) {
Modifier.clickable(
role = Role.Button,
onClick = onClick,
)
} else {
Modifier
},
),
contentScale = ContentScale.Crop,
)
}
}
private val CoverPlaceholderColor = Color(0x1F888888)

View File

@@ -45,11 +45,11 @@ import coil.request.ImageRequest
import coil.size.Size
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.manga.EditCoverAction
import eu.kanade.presentation.util.clickableNoIndication
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.util.clickableNoIndication
@Composable
fun MangaCoverDialog(

View File

@@ -74,15 +74,14 @@ import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.google.accompanist.flowlayout.FlowRow
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.components.TextButton
import eu.kanade.presentation.util.clickableNoIndication
import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.clickableNoIndication
import tachiyomi.presentation.core.util.secondaryItemAlpha
import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))