MangaController overhaul (#7244)
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.util.quantityStringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
|
||||
@Composable
|
||||
fun ChapterHeader(
|
||||
chapterCount: Int?,
|
||||
isChapterFiltered: Boolean,
|
||||
onFilterButtonClicked: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = if (chapterCount == null) {
|
||||
stringResource(id = R.string.chapters)
|
||||
} else {
|
||||
quantityStringResource(id = R.plurals.manga_num_chapters, quantity = chapterCount)
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
|
||||
IconButton(onClick = onFilterButtonClicked) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FilterList,
|
||||
contentDescription = stringResource(id = R.string.action_filter),
|
||||
tint = if (isChapterFiltered) {
|
||||
Color(LocalContext.current.getResourceColor(R.attr.colorFilterActive))
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onBackground
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun DotSeparatorText() {
|
||||
Text(text = " • ")
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
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.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BookmarkAdd
|
||||
import androidx.compose.material.icons.filled.BookmarkRemove
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.DoneAll
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.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.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.tachiyomi.R
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun MangaBottomActionMenu(
|
||||
visible: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onBookmarkClicked: (() -> Unit)?,
|
||||
onRemoveBookmarkClicked: (() -> Unit)?,
|
||||
onMarkAsReadClicked: (() -> Unit)?,
|
||||
onMarkAsUnreadClicked: (() -> Unit)?,
|
||||
onMarkPreviousAsReadClicked: (() -> Unit)?,
|
||||
onDownloadClicked: (() -> Unit)?,
|
||||
onDeleteClicked: (() -> Unit)?,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = expandVertically(expandFrom = Alignment.Bottom),
|
||||
exit = shrinkVertically(shrinkTowards = Alignment.Bottom),
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
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(1000)
|
||||
if (isActive) confirm[toConfirmIndex] = false
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 8.dp, vertical = 12.dp),
|
||||
) {
|
||||
if (onBookmarkClicked != null) {
|
||||
Button(
|
||||
title = stringResource(id = R.string.action_bookmark),
|
||||
icon = Icons.Default.BookmarkAdd,
|
||||
toConfirm = confirm[0],
|
||||
onLongClick = { onLongClickItem(0) },
|
||||
onClick = onBookmarkClicked,
|
||||
)
|
||||
}
|
||||
if (onRemoveBookmarkClicked != null) {
|
||||
Button(
|
||||
title = stringResource(id = R.string.action_remove_bookmark),
|
||||
icon = Icons.Default.BookmarkRemove,
|
||||
toConfirm = confirm[1],
|
||||
onLongClick = { onLongClickItem(1) },
|
||||
onClick = onRemoveBookmarkClicked,
|
||||
)
|
||||
}
|
||||
if (onMarkAsReadClicked != null) {
|
||||
Button(
|
||||
title = stringResource(id = R.string.action_mark_as_read),
|
||||
icon = Icons.Default.DoneAll,
|
||||
toConfirm = confirm[2],
|
||||
onLongClick = { onLongClickItem(2) },
|
||||
onClick = onMarkAsReadClicked,
|
||||
)
|
||||
}
|
||||
if (onMarkAsUnreadClicked != null) {
|
||||
Button(
|
||||
title = stringResource(id = R.string.action_mark_as_unread),
|
||||
icon = Icons.Default.RemoveDone,
|
||||
toConfirm = confirm[3],
|
||||
onLongClick = { onLongClickItem(3) },
|
||||
onClick = onMarkAsUnreadClicked,
|
||||
)
|
||||
}
|
||||
if (onMarkPreviousAsReadClicked != null) {
|
||||
Button(
|
||||
title = stringResource(id = 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(id = R.string.action_download),
|
||||
icon = Icons.Default.Download,
|
||||
toConfirm = confirm[5],
|
||||
onLongClick = { onLongClickItem(5) },
|
||||
onClick = onDownloadClicked,
|
||||
)
|
||||
}
|
||||
if (onDeleteClicked != null) {
|
||||
Button(
|
||||
title = stringResource(id = R.string.action_delete),
|
||||
icon = Icons.Default.Delete,
|
||||
toConfirm = confirm[6],
|
||||
onLongClick = { onLongClickItem(6) },
|
||||
onClick = onDeleteClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.Button(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
toConfirm: Boolean,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
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.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
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.ChapterDownloadIndicator
|
||||
import eu.kanade.presentation.manga.ChapterDownloadAction
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
|
||||
@Composable
|
||||
fun MangaChapterListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
date: String?,
|
||||
readProgress: String?,
|
||||
scanlator: String?,
|
||||
read: Boolean,
|
||||
bookmark: Boolean,
|
||||
selected: Boolean,
|
||||
downloadState: Download.State,
|
||||
downloadProgress: Int,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onDownloadClick: ((ChapterDownloadAction) -> Unit)?,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(if (selected) MaterialTheme.colorScheme.surfaceVariant else Color.Transparent)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.alpha(if (read) ReadItemAlpha else 1f),
|
||||
) {
|
||||
val textColor = if (bookmark) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
var textHeight by remember { mutableStateOf(0) }
|
||||
if (bookmark) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bookmark,
|
||||
contentDescription = stringResource(id = R.string.action_filter_bookmarked),
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||
tint = textColor,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
.copy(color = textColor),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Row {
|
||||
ProvideTextStyle(
|
||||
value = MaterialTheme.typography.bodyMedium
|
||||
.copy(color = textColor, fontSize = 12.sp),
|
||||
) {
|
||||
if (date != null) {
|
||||
Text(
|
||||
text = date,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (readProgress != null || scanlator != null) DotSeparatorText()
|
||||
}
|
||||
if (readProgress != null) {
|
||||
Text(
|
||||
text = readProgress,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.alpha(ReadItemAlpha),
|
||||
)
|
||||
if (scanlator != null) DotSeparatorText()
|
||||
}
|
||||
if (scanlator != null) {
|
||||
Text(
|
||||
text = scanlator,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Download view
|
||||
if (onDownloadClick != null) {
|
||||
ChapterDownloadIndicator(
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadState = downloadState,
|
||||
downloadProgress = downloadProgress,
|
||||
onClick = onDownloadClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val ReadItemAlpha = .38f
|
||||
@@ -0,0 +1,616 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||
import androidx.compose.foundation.background
|
||||
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.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AttachMoney
|
||||
import androidx.compose.material.icons.filled.Block
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material.icons.filled.DoneAll
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.Public
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.SuggestionChip
|
||||
import androidx.compose.material3.SuggestionChipDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
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.clipToBounds
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.presentation.components.MangaCover
|
||||
import eu.kanade.presentation.components.TextButton
|
||||
import eu.kanade.presentation.util.clickableNoIndication
|
||||
import eu.kanade.presentation.util.quantityStringResource
|
||||
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 kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun MangaInfoHeader(
|
||||
modifier: Modifier = Modifier,
|
||||
windowWidthSizeClass: WindowWidthSizeClass,
|
||||
appBarPadding: Dp,
|
||||
title: String,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
coverDataProvider: () -> Manga,
|
||||
favorite: Boolean,
|
||||
status: Long,
|
||||
trackingCount: Int,
|
||||
fromSource: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onTagClicked: (String) -> Unit,
|
||||
onEditCategory: (() -> Unit)?,
|
||||
onCoverClick: () -> Unit,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Column(modifier = modifier) {
|
||||
Box {
|
||||
// Backdrop
|
||||
val backdropGradientColors = listOf(
|
||||
Color.Transparent,
|
||||
MaterialTheme.colorScheme.background,
|
||||
)
|
||||
AsyncImage(
|
||||
model = coverDataProvider(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
brush = Brush.verticalGradient(colors = backdropGradientColors),
|
||||
)
|
||||
}
|
||||
.alpha(.2f),
|
||||
)
|
||||
|
||||
// Manga & source info
|
||||
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
|
||||
MangaAndSourceTitlesSmall(
|
||||
appBarPadding = appBarPadding,
|
||||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = context,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
status = status,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
)
|
||||
} else {
|
||||
MangaAndSourceTitlesLarge(
|
||||
appBarPadding = appBarPadding,
|
||||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = context,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
status = status,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
Row(modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
MangaActionButton(
|
||||
title = if (favorite) {
|
||||
stringResource(id = R.string.in_library)
|
||||
} else {
|
||||
stringResource(id = R.string.add_to_library)
|
||||
},
|
||||
icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
|
||||
color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
)
|
||||
if (onTrackingClicked != null) {
|
||||
MangaActionButton(
|
||||
title = if (trackingCount == 0) {
|
||||
stringResource(id = R.string.manga_tracking_tab)
|
||||
} else {
|
||||
quantityStringResource(id = R.plurals.num_trackers, quantity = trackingCount, trackingCount)
|
||||
},
|
||||
icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done,
|
||||
color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
|
||||
onClick = onTrackingClicked,
|
||||
)
|
||||
}
|
||||
if (onWebViewClicked != null) {
|
||||
MangaActionButton(
|
||||
title = stringResource(id = R.string.action_web_view),
|
||||
icon = Icons.Default.Public,
|
||||
color = defaultActionButtonColor,
|
||||
onClick = onWebViewClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Expandable description-tags
|
||||
Column {
|
||||
val (expanded, onExpanded) = rememberSaveable {
|
||||
mutableStateOf(fromSource || windowWidthSizeClass != WindowWidthSizeClass.Compact)
|
||||
}
|
||||
if (!description.isNullOrBlank()) {
|
||||
val trimmedDescription = remember(description) {
|
||||
description
|
||||
.replace(Regex(" +\$", setOf(RegexOption.MULTILINE)), "")
|
||||
.replace(Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)), "\n")
|
||||
}
|
||||
MangaSummary(
|
||||
expandedDescription = description,
|
||||
shrunkDescription = trimmedDescription,
|
||||
expanded = expanded,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(description, description) },
|
||||
onClick = { onExpanded(!expanded) },
|
||||
),
|
||||
)
|
||||
}
|
||||
val tags = tagsProvider()
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(vertical = 12.dp)
|
||||
.animateContentSize(),
|
||||
) {
|
||||
if (expanded) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
mainAxisSpacing = 4.dp,
|
||||
crossAxisSpacing = 8.dp,
|
||||
) {
|
||||
tags.forEach {
|
||||
TagsChip(
|
||||
text = it,
|
||||
onClick = { onTagClicked(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items(items = tags) {
|
||||
TagsChip(
|
||||
text = it,
|
||||
onClick = { onTagClicked(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaAndSourceTitlesLarge(
|
||||
appBarPadding: Dp,
|
||||
coverDataProvider: () -> Manga,
|
||||
onCoverClick: () -> Unit,
|
||||
title: String,
|
||||
context: Context,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
status: Long,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier.fillMaxWidth(0.4f),
|
||||
data = coverDataProvider(),
|
||||
onClick = onCoverClick,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = title.takeIf { it.isNotBlank() } ?: stringResource(id = R.string.unknown),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.clickableNoIndication(
|
||||
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) },
|
||||
onClick = { if (title.isNotBlank()) doSearch(title, true) },
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = author?.takeIf { it.isNotBlank() } ?: stringResource(id = R.string.unknown_author),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (!author.isNullOrBlank()) context.copyToClipboard(
|
||||
author,
|
||||
author,
|
||||
)
|
||||
},
|
||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
if (!artist.isNullOrBlank()) {
|
||||
Text(
|
||||
text = artist,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(artist, artist) },
|
||||
onClick = { doSearch(artist, true) },
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SManga.ONGOING.toLong() -> Icons.Default.Schedule
|
||||
SManga.COMPLETED.toLong() -> Icons.Default.DoneAll
|
||||
SManga.LICENSED.toLong() -> Icons.Default.AttachMoney
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done
|
||||
SManga.CANCELLED.toLong() -> Icons.Default.Close
|
||||
SManga.ON_HIATUS.toLong() -> Icons.Default.Pause
|
||||
else -> Icons.Default.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
)
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
Text(
|
||||
text = when (status) {
|
||||
SManga.ONGOING.toLong() -> stringResource(id = R.string.ongoing)
|
||||
SManga.COMPLETED.toLong() -> stringResource(id = R.string.completed)
|
||||
SManga.LICENSED.toLong() -> stringResource(id = R.string.licensed)
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(id = R.string.publishing_finished)
|
||||
SManga.CANCELLED.toLong() -> stringResource(id = R.string.cancelled)
|
||||
SManga.ON_HIATUS.toLong() -> stringResource(id = R.string.on_hiatus)
|
||||
else -> stringResource(id = R.string.unknown)
|
||||
},
|
||||
)
|
||||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = sourceName,
|
||||
modifier = Modifier.clickableNoIndication { doSearch(sourceName, false) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaAndSourceTitlesSmall(
|
||||
appBarPadding: Dp,
|
||||
coverDataProvider: () -> Manga,
|
||||
onCoverClick: () -> Unit,
|
||||
title: String,
|
||||
context: Context,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
status: Long,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier.sizeIn(maxWidth = 100.dp),
|
||||
data = coverDataProvider(),
|
||||
onClick = onCoverClick,
|
||||
)
|
||||
Column(modifier = Modifier.padding(start = 16.dp)) {
|
||||
Text(
|
||||
text = title.ifBlank { stringResource(id = R.string.unknown) },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.clickableNoIndication(
|
||||
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) },
|
||||
onClick = { if (title.isNotBlank()) doSearch(title, true) },
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = author?.takeIf { it.isNotBlank() } ?: stringResource(id = R.string.unknown_author),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (!author.isNullOrBlank()) context.copyToClipboard(
|
||||
author,
|
||||
author,
|
||||
)
|
||||
},
|
||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||
),
|
||||
)
|
||||
if (!artist.isNullOrBlank()) {
|
||||
Text(
|
||||
text = artist,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(artist, artist) },
|
||||
onClick = { doSearch(artist, true) },
|
||||
),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SManga.ONGOING.toLong() -> Icons.Default.Schedule
|
||||
SManga.COMPLETED.toLong() -> Icons.Default.DoneAll
|
||||
SManga.LICENSED.toLong() -> Icons.Default.AttachMoney
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done
|
||||
SManga.CANCELLED.toLong() -> Icons.Default.Close
|
||||
SManga.ON_HIATUS.toLong() -> Icons.Default.Pause
|
||||
else -> Icons.Default.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
)
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
Text(
|
||||
text = when (status) {
|
||||
SManga.ONGOING.toLong() -> stringResource(id = R.string.ongoing)
|
||||
SManga.COMPLETED.toLong() -> stringResource(id = R.string.completed)
|
||||
SManga.LICENSED.toLong() -> stringResource(id = R.string.licensed)
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(id = R.string.publishing_finished)
|
||||
SManga.CANCELLED.toLong() -> stringResource(id = R.string.cancelled)
|
||||
SManga.ON_HIATUS.toLong() -> stringResource(id = R.string.on_hiatus)
|
||||
else -> stringResource(id = R.string.unknown)
|
||||
},
|
||||
)
|
||||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = sourceName,
|
||||
modifier = Modifier.clickableNoIndication { doSearch(sourceName, false) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaSummary(
|
||||
expandedDescription: String,
|
||||
shrunkDescription: String,
|
||||
expanded: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expandedHeight by remember { mutableStateOf(0) }
|
||||
var shrunkHeight by remember { mutableStateOf(0) }
|
||||
val heightDelta = remember(expandedHeight, shrunkHeight) { expandedHeight - shrunkHeight }
|
||||
val animProgress by animateFloatAsState(if (expanded) 1f else 0f)
|
||||
val scrimHeight = with(LocalDensity.current) { remember { 24.sp.roundToPx() } }
|
||||
|
||||
SubcomposeLayout(modifier = modifier.clipToBounds()) { constraints ->
|
||||
val shrunkPlaceable = subcompose("description-s") {
|
||||
Text(
|
||||
text = "\n\n", // Shows at least 3 lines
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}.map { it.measure(constraints) }
|
||||
shrunkHeight = shrunkPlaceable.maxByOrNull { it.height }?.height ?: 0
|
||||
|
||||
val expandedPlaceable = subcompose("description-l") {
|
||||
Text(
|
||||
text = expandedDescription,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}.map { it.measure(constraints) }
|
||||
expandedHeight = expandedPlaceable.maxByOrNull { it.height }?.height?.coerceAtLeast(shrunkHeight) ?: 0
|
||||
|
||||
val actualPlaceable = subcompose("description") {
|
||||
Text(
|
||||
text = if (expanded) expandedDescription else shrunkDescription,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
)
|
||||
}.map { it.measure(constraints) }
|
||||
|
||||
val scrimPlaceable = subcompose("scrim") {
|
||||
val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background)
|
||||
Box(
|
||||
modifier = Modifier.background(Brush.verticalGradient(colors = colors)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
|
||||
Icon(
|
||||
painter = rememberAnimatedVectorPainter(image, !expanded),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())),
|
||||
)
|
||||
}
|
||||
}.map { it.measure(Constraints.fixed(width = constraints.maxWidth, height = scrimHeight)) }
|
||||
|
||||
val currentHeight = shrunkHeight + ((heightDelta + scrimHeight) * animProgress).roundToInt()
|
||||
layout(constraints.maxWidth, currentHeight) {
|
||||
actualPlaceable.forEach {
|
||||
it.place(0, 0)
|
||||
}
|
||||
|
||||
val scrimY = currentHeight - scrimHeight
|
||||
scrimPlaceable.forEach {
|
||||
it.place(0, scrimY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TagsChip(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
|
||||
SuggestionChip(
|
||||
onClick = onClick,
|
||||
label = { Text(text = text, style = MaterialTheme.typography.bodySmall) },
|
||||
border = null,
|
||||
colors = SuggestionChipDefaults.suggestionChipColors(
|
||||
containerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
labelColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.MangaActionButton(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
color: Color,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
onLongClick = onLongClick,
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = title,
|
||||
color = color,
|
||||
fontSize = 12.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
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.Download
|
||||
import androidx.compose.material.icons.filled.FlipToBack
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallTopAppBar
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.manga.DownloadAction
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun MangaSmallAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
titleAlphaProvider: () -> Float,
|
||||
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
|
||||
incognitoMode: Boolean,
|
||||
downloadedOnlyMode: Boolean,
|
||||
onBackClicked: () -> Unit,
|
||||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onSelectAll: () -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
val isActionMode = actionModeCounter > 0
|
||||
val backgroundAlpha = if (isActionMode) 1f else backgroundAlphaProvider()
|
||||
val backgroundColor by TopAppBarDefaults.centerAlignedTopAppBarColors().containerColor(1f)
|
||||
Column(
|
||||
modifier = modifier.drawBehind {
|
||||
drawRect(backgroundColor.copy(alpha = backgroundAlpha))
|
||||
},
|
||||
) {
|
||||
SmallTopAppBar(
|
||||
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)),
|
||||
title = {
|
||||
Text(
|
||||
text = if (isActionMode) actionModeCounter.toString() else title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.alpha(titleAlphaProvider()),
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClicked) {
|
||||
Icon(
|
||||
imageVector = if (isActionMode) Icons.Default.Close else Icons.Default.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.abc_action_bar_up_description),
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (isActionMode) {
|
||||
IconButton(onClick = onSelectAll) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = stringResource(id = R.string.action_select_all),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onInvertSelection) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FlipToBack,
|
||||
contentDescription = stringResource(id = R.string.action_select_inverse),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (onShareClicked != null) {
|
||||
IconButton(onClick = onShareClicked) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Share,
|
||||
contentDescription = stringResource(id = R.string.action_share),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (onDownloadClicked != null) {
|
||||
val (downloadExpanded, onDownloadExpanded) = remember { mutableStateOf(false) }
|
||||
Box {
|
||||
IconButton(onClick = { onDownloadExpanded(!downloadExpanded) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Download,
|
||||
contentDescription = stringResource(id = R.string.manga_download),
|
||||
)
|
||||
}
|
||||
val onDismissRequest = { onDownloadExpanded(false) }
|
||||
DropdownMenu(
|
||||
expanded = downloadExpanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.download_1)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.NEXT_1_CHAPTER)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.download_5)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.NEXT_5_CHAPTERS)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.download_10)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.NEXT_10_CHAPTERS)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.download_custom)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.CUSTOM)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.download_unread)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.UNREAD_CHAPTERS)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.download_all)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.ALL_CHAPTERS)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onEditCategoryClicked != null && onMigrateClicked != null) {
|
||||
val (moreExpanded, onMoreExpanded) = remember { mutableStateOf(false) }
|
||||
Box {
|
||||
IconButton(onClick = { onMoreExpanded(!moreExpanded) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(id = R.string.abc_action_menu_overflow_description),
|
||||
)
|
||||
}
|
||||
val onDismissRequest = { onMoreExpanded(false) }
|
||||
DropdownMenu(
|
||||
expanded = moreExpanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.action_edit_categories)) },
|
||||
onClick = {
|
||||
onEditCategoryClicked()
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.action_migrate)) },
|
||||
onClick = {
|
||||
onMigrateClicked()
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Background handled by parent
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
|
||||
if (downloadedOnlyMode) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.label_downloaded_only),
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.tertiary)
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
color = MaterialTheme.colorScheme.onTertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
if (incognitoMode) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.pref_incognito_mode),
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.primary)
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun MangaTopAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
coverDataProvider: () -> Manga,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
favorite: Boolean,
|
||||
status: Long,
|
||||
trackingCount: Int,
|
||||
chapterCount: Int?,
|
||||
chapterFiltered: Boolean,
|
||||
incognitoMode: Boolean,
|
||||
downloadedOnlyMode: Boolean,
|
||||
fromSource: Boolean,
|
||||
onBackClicked: () -> Unit,
|
||||
onCoverClick: () -> Unit,
|
||||
onTagClicked: (String) -> Unit,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onFilterButtonClicked: () -> Unit,
|
||||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
doGlobalSearch: (query: String, global: Boolean) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior?,
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onSelectAll: () -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
onSmallAppBarHeightChanged: (Int) -> Unit,
|
||||
) {
|
||||
val scrollPercentageProvider = { scrollBehavior?.scrollFraction?.coerceIn(0f, 1f) ?: 0f }
|
||||
val inverseScrollPercentageProvider = { 1f - scrollPercentageProvider() }
|
||||
|
||||
Layout(
|
||||
modifier = modifier,
|
||||
content = {
|
||||
val (smallHeightPx, onSmallHeightPxChanged) = remember { mutableStateOf(0) }
|
||||
Column(modifier = Modifier.layoutId("mangaInfo")) {
|
||||
MangaInfoHeader(
|
||||
windowWidthSizeClass = WindowWidthSizeClass.Compact,
|
||||
appBarPadding = with(LocalDensity.current) { smallHeightPx.toDp() },
|
||||
title = title,
|
||||
author = author,
|
||||
artist = artist,
|
||||
description = description,
|
||||
tagsProvider = tagsProvider,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
coverDataProvider = coverDataProvider,
|
||||
favorite = favorite,
|
||||
status = status,
|
||||
trackingCount = trackingCount,
|
||||
fromSource = fromSource,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onTagClicked = onTagClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
onCoverClick = onCoverClick,
|
||||
doSearch = doGlobalSearch,
|
||||
)
|
||||
ChapterHeader(
|
||||
chapterCount = chapterCount,
|
||||
isChapterFiltered = chapterFiltered,
|
||||
onFilterButtonClicked = onFilterButtonClicked,
|
||||
)
|
||||
}
|
||||
|
||||
MangaSmallAppBar(
|
||||
modifier = Modifier
|
||||
.layoutId("topBar")
|
||||
.onSizeChanged {
|
||||
onSmallHeightPxChanged(it.height)
|
||||
onSmallAppBarHeightChanged(it.height)
|
||||
},
|
||||
title = title,
|
||||
titleAlphaProvider = { if (actionModeCounter == 0) scrollPercentageProvider() else 1f },
|
||||
incognitoMode = incognitoMode,
|
||||
downloadedOnlyMode = downloadedOnlyMode,
|
||||
onBackClicked = onBackClicked,
|
||||
onShareClicked = onShareClicked,
|
||||
onDownloadClicked = onDownloadClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
actionModeCounter = actionModeCounter,
|
||||
onSelectAll = onSelectAll,
|
||||
onInvertSelection = onInvertSelection,
|
||||
)
|
||||
},
|
||||
) { measurables, constraints ->
|
||||
val mangaInfoPlaceable = measurables
|
||||
.first { it.layoutId == "mangaInfo" }
|
||||
.measure(constraints.copy(maxHeight = Constraints.Infinity))
|
||||
val topBarPlaceable = measurables
|
||||
.first { it.layoutId == "topBar" }
|
||||
.measure(constraints)
|
||||
val mangaInfoHeight = mangaInfoPlaceable.height
|
||||
val topBarHeight = topBarPlaceable.height
|
||||
val mangaInfoSansTopBarHeightPx = mangaInfoHeight - topBarHeight
|
||||
val layoutHeight = topBarHeight +
|
||||
(mangaInfoSansTopBarHeightPx * inverseScrollPercentageProvider()).roundToInt()
|
||||
|
||||
layout(constraints.maxWidth, layoutHeight) {
|
||||
val mangaInfoY = (-mangaInfoSansTopBarHeightPx * scrollPercentageProvider()).roundToInt()
|
||||
mangaInfoPlaceable.place(0, mangaInfoY)
|
||||
topBarPlaceable.place(0, 0)
|
||||
|
||||
// Update offset limit
|
||||
val offsetLimit = -mangaInfoSansTopBarHeightPx.toFloat()
|
||||
if (scrollBehavior?.state?.offsetLimit != offsetLimit) {
|
||||
scrollBehavior?.state?.offsetLimit = offsetLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user