MangaScreen: Ditch the expanded app bar (#7470)
Animating the content padding that's used for the lazy list is heavy. A simple fix to *just* offset the list is blocked by a Compose fling issue (b/179417109). So I decided to go with the previous layout of this screen by putting everything in the list. MangaInfoHeader is split into separate composables to avoid jank when the item is being inflated. (cherry picked from commit 34906a74253e7463ac23ea96496c59198884e0be) # Conflicts: # app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt # app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt # app/src/main/java/eu/kanade/presentation/manga/components/MangaTopAppBar.kt
This commit is contained in:
parent
4e29fd5b2a
commit
d5aecaad21
@ -2,15 +2,11 @@ package eu.kanade.presentation.manga
|
|||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.rememberSplineBasedDecay
|
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
|
||||||
import androidx.compose.foundation.gestures.rememberScrollableState
|
|
||||||
import androidx.compose.foundation.gestures.scrollBy
|
|
||||||
import androidx.compose.foundation.gestures.scrollable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
@ -36,12 +32,10 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberTopAppBarScrollState
|
|
||||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.Stable
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
@ -49,7 +43,6 @@ import androidx.compose.runtime.toMutableStateList
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
@ -65,13 +58,14 @@ import eu.kanade.presentation.components.Scaffold
|
|||||||
import eu.kanade.presentation.components.SwipeRefreshIndicator
|
import eu.kanade.presentation.components.SwipeRefreshIndicator
|
||||||
import eu.kanade.presentation.components.VerticalFastScroller
|
import eu.kanade.presentation.components.VerticalFastScroller
|
||||||
import eu.kanade.presentation.manga.components.ChapterHeader
|
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.MangaBottomActionMenu
|
||||||
import eu.kanade.presentation.manga.components.MangaChapterListItem
|
import eu.kanade.presentation.manga.components.MangaChapterListItem
|
||||||
import eu.kanade.presentation.manga.components.MangaInfoHeader
|
import eu.kanade.presentation.manga.components.MangaInfoBox
|
||||||
|
import eu.kanade.presentation.manga.components.MangaInfoButtons
|
||||||
import eu.kanade.presentation.manga.components.MangaSmallAppBar
|
import eu.kanade.presentation.manga.components.MangaSmallAppBar
|
||||||
import eu.kanade.presentation.manga.components.MangaTopAppBar
|
|
||||||
import eu.kanade.presentation.manga.components.SearchMetadataChips
|
import eu.kanade.presentation.manga.components.SearchMetadataChips
|
||||||
import eu.kanade.presentation.util.ExitUntilCollapsedScrollBehavior
|
|
||||||
import eu.kanade.presentation.util.isScrolledToEnd
|
import eu.kanade.presentation.util.isScrolledToEnd
|
||||||
import eu.kanade.presentation.util.isScrollingUp
|
import eu.kanade.presentation.util.isScrollingUp
|
||||||
import eu.kanade.presentation.util.plus
|
import eu.kanade.presentation.util.plus
|
||||||
@ -85,7 +79,6 @@ import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
|||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import exh.source.getMainSource
|
import exh.source.getMainSource
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import java.text.DecimalFormatSymbols
|
import java.text.DecimalFormatSymbols
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@ -238,175 +231,214 @@ private fun MangaScreenSmallImpl(
|
|||||||
onMultiDeleteClicked: (List<Chapter>) -> Unit,
|
onMultiDeleteClicked: (List<Chapter>) -> Unit,
|
||||||
) {
|
) {
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
|
|
||||||
val scrollBehavior = ExitUntilCollapsedScrollBehavior(rememberTopAppBarScrollState(), decayAnimationSpec)
|
|
||||||
val chapterListState = rememberLazyListState()
|
val chapterListState = rememberLazyListState()
|
||||||
SideEffect {
|
|
||||||
if (chapterListState.firstVisibleItemIndex > 0 || chapterListState.firstVisibleItemScrollOffset > 0) {
|
|
||||||
// Should go here after a configuration change
|
|
||||||
// Safe to say that the app bar is fully scrolled
|
|
||||||
scrollBehavior.state.offset = scrollBehavior.state.offsetLimit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||||
val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(1) }
|
val chapters = remember(state) { state.processedChapters.toList() }
|
||||||
SwipeRefresh(
|
val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() }
|
||||||
state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter),
|
val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list
|
||||||
onRefresh = onRefresh,
|
// SY -->
|
||||||
indicatorPadding = PaddingValues(
|
val metadataSource = remember(state.source.id) { state.source.getMainSource<MetadataSource<*, *>>() }
|
||||||
start = insetPadding.calculateStartPadding(layoutDirection),
|
// SY <--
|
||||||
top = with(LocalDensity.current) { topBarHeight.toDp() },
|
|
||||||
end = insetPadding.calculateEndPadding(layoutDirection),
|
val internalOnBackPressed = {
|
||||||
),
|
if (selected.isNotEmpty()) {
|
||||||
indicator = { s, trigger ->
|
selected.clear()
|
||||||
SwipeRefreshIndicator(
|
} else {
|
||||||
state = s,
|
onBackClicked()
|
||||||
refreshTriggerDistance = trigger,
|
}
|
||||||
|
}
|
||||||
|
BackHandler(onBack = internalOnBackPressed)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(insetPadding),
|
||||||
|
topBar = {
|
||||||
|
val firstVisibleItemIndex by remember {
|
||||||
|
derivedStateOf { chapterListState.firstVisibleItemIndex }
|
||||||
|
}
|
||||||
|
val firstVisibleItemScrollOffset by remember {
|
||||||
|
derivedStateOf { chapterListState.firstVisibleItemScrollOffset }
|
||||||
|
}
|
||||||
|
val animatedTitleAlpha by animateFloatAsState(
|
||||||
|
if (firstVisibleItemIndex > 0) 1f else 0f,
|
||||||
|
)
|
||||||
|
val animatedBgAlpha by animateFloatAsState(
|
||||||
|
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
||||||
|
)
|
||||||
|
MangaSmallAppBar(
|
||||||
|
title = state.manga.title,
|
||||||
|
titleAlphaProvider = { animatedTitleAlpha },
|
||||||
|
backgroundAlphaProvider = { animatedBgAlpha },
|
||||||
|
incognitoMode = state.isIncognitoMode,
|
||||||
|
downloadedOnlyMode = state.isDownloadedOnlyMode,
|
||||||
|
onBackClicked = onBackClicked,
|
||||||
|
onShareClicked = onShareClicked,
|
||||||
|
onDownloadClicked = onDownloadActionClicked,
|
||||||
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
|
onMigrateClicked = onMigrateClicked,
|
||||||
|
// SY -->
|
||||||
|
showEditInfo = state.manga.favorite,
|
||||||
|
onEditInfoClicked = onEditInfoClicked,
|
||||||
|
showRecommends = state.showRecommendationsInOverflow,
|
||||||
|
onRecommendClicked = onRecommendClicked,
|
||||||
|
showMergeSettings = state.manga.source == MERGED_SOURCE_ID,
|
||||||
|
onMergedSettingsClicked = onMergedSettingsClicked,
|
||||||
|
// SY <--
|
||||||
|
actionModeCounter = selected.size,
|
||||||
|
onSelectAll = {
|
||||||
|
selected.clear()
|
||||||
|
selected.addAll(chapters)
|
||||||
|
},
|
||||||
|
onInvertSelection = {
|
||||||
|
val toSelect = chapters - selected
|
||||||
|
selected.clear()
|
||||||
|
selected.addAll(toSelect)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
bottomBar = {
|
||||||
val chapters = remember(state) { state.processedChapters.toList() }
|
SharedMangaBottomActionMenu(
|
||||||
val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() }
|
selected = selected,
|
||||||
val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
|
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||||
val internalOnBackPressed = {
|
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
||||||
if (selected.isNotEmpty()) {
|
onDownloadChapter = onDownloadChapter,
|
||||||
selected.clear()
|
onMultiDeleteClicked = onMultiDeleteClicked,
|
||||||
} else {
|
fillFraction = 1f,
|
||||||
onBackClicked()
|
)
|
||||||
}
|
},
|
||||||
}
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
BackHandler(onBack = internalOnBackPressed)
|
floatingActionButton = {
|
||||||
|
AnimatedVisibility(
|
||||||
Scaffold(
|
visible = chapters.any { !it.chapter.read } && selected.isEmpty(),
|
||||||
modifier = Modifier
|
enter = fadeIn(),
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
exit = fadeOut(),
|
||||||
.padding(insetPadding),
|
) {
|
||||||
topBar = {
|
ExtendedFloatingActionButton(
|
||||||
MangaTopAppBar(
|
text = {
|
||||||
|
val id = if (chapters.any { it.chapter.read }) {
|
||||||
|
R.string.action_resume
|
||||||
|
} else {
|
||||||
|
R.string.action_start
|
||||||
|
}
|
||||||
|
Text(text = stringResource(id))
|
||||||
|
},
|
||||||
|
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
|
||||||
|
onClick = onContinueReading,
|
||||||
|
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.scrollable(
|
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
|
||||||
state = rememberScrollableState {
|
)
|
||||||
var consumed = runBlocking { chapterListState.scrollBy(-it) } * -1
|
}
|
||||||
if (consumed == 0f) {
|
},
|
||||||
// Pass scroll to app bar if we're on the top of the list
|
) { contentPadding ->
|
||||||
val newOffset =
|
val noTopContentPadding = PaddingValues(
|
||||||
(scrollBehavior.state.offset + it).coerceIn(scrollBehavior.state.offsetLimit, 0f)
|
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||||
consumed = newOffset - scrollBehavior.state.offset
|
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||||
scrollBehavior.state.offset = newOffset
|
bottom = contentPadding.calculateBottomPadding(),
|
||||||
}
|
) + WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
||||||
consumed
|
val topPadding = contentPadding.calculateTopPadding()
|
||||||
},
|
|
||||||
orientation = Orientation.Vertical,
|
SwipeRefresh(
|
||||||
interactionSource = chapterListState.interactionSource as MutableInteractionSource,
|
state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter),
|
||||||
),
|
onRefresh = onRefresh,
|
||||||
title = state.manga.title,
|
indicatorPadding = contentPadding,
|
||||||
author = state.manga.author,
|
indicator = { s, trigger ->
|
||||||
artist = state.manga.artist,
|
SwipeRefreshIndicator(
|
||||||
description = state.manga.description,
|
state = s,
|
||||||
tagsProvider = { state.manga.genre },
|
refreshTriggerDistance = trigger,
|
||||||
coverDataProvider = { state.manga },
|
|
||||||
sourceName = remember { state.source.getNameForMangaInfo() },
|
|
||||||
isStubSource = remember { state.source is SourceManager.StubSource },
|
|
||||||
favorite = state.manga.favorite,
|
|
||||||
status = state.manga.status,
|
|
||||||
trackingCount = state.trackingCount,
|
|
||||||
chapterCount = chapters.size,
|
|
||||||
chapterFiltered = state.manga.chaptersFiltered(),
|
|
||||||
incognitoMode = state.isIncognitoMode,
|
|
||||||
downloadedOnlyMode = state.isDownloadedOnlyMode,
|
|
||||||
fromSource = state.isFromSource,
|
|
||||||
onBackClicked = internalOnBackPressed,
|
|
||||||
onCoverClick = onCoverClicked,
|
|
||||||
onTagClicked = onTagClicked,
|
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
|
||||||
onWebViewClicked = onWebViewClicked,
|
|
||||||
onTrackingClicked = onTrackingClicked,
|
|
||||||
onFilterButtonClicked = onFilterButtonClicked,
|
|
||||||
onShareClicked = onShareClicked,
|
|
||||||
onDownloadClicked = onDownloadActionClicked,
|
|
||||||
onEditCategoryClicked = onEditCategoryClicked,
|
|
||||||
onMigrateClicked = onMigrateClicked,
|
|
||||||
onEditInfoClicked = onEditInfoClicked,
|
|
||||||
onRecommendClicked = onRecommendClicked,
|
|
||||||
showRecommendsInOverflow = state.showRecommendationsInOverflow,
|
|
||||||
showMergedSettings = state.manga.source == MERGED_SOURCE_ID,
|
|
||||||
onMergedSettingsClicked = onMergedSettingsClicked,
|
|
||||||
onMergeClicked = onMergeClicked,
|
|
||||||
doGlobalSearch = onSearch,
|
|
||||||
showMergeWithAnother = state.showMergeWithAnother,
|
|
||||||
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
|
||||||
mangaMetadataHeader = getDescriptionComposable(
|
|
||||||
source = remember { state.source.getMainSource<MetadataSource<*, *>>() },
|
|
||||||
state = state,
|
|
||||||
openMetadataViewer = onMetadataViewerClicked,
|
|
||||||
search = { onSearch(it, false) },
|
|
||||||
),
|
|
||||||
searchMetadataChips = remember(state) { SearchMetadataChips(state.meta, state.source, state.manga.genre) },
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
actionModeCounter = selected.size,
|
|
||||||
onSelectAll = {
|
|
||||||
selected.clear()
|
|
||||||
selected.addAll(chapters)
|
|
||||||
},
|
|
||||||
onInvertSelection = {
|
|
||||||
val toSelect = chapters - selected
|
|
||||||
selected.clear()
|
|
||||||
selected.addAll(toSelect)
|
|
||||||
},
|
|
||||||
onSmallAppBarHeightChanged = onTopBarHeightChanged,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
) {
|
||||||
SharedMangaBottomActionMenu(
|
|
||||||
selected = selected,
|
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
|
||||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
|
||||||
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
|
||||||
onDownloadChapter = onDownloadChapter,
|
|
||||||
onMultiDeleteClicked = onMultiDeleteClicked,
|
|
||||||
fillFraction = 1f,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
|
||||||
floatingActionButton = {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = chapters.any { !it.chapter.read } && selected.isEmpty(),
|
|
||||||
enter = fadeIn(),
|
|
||||||
exit = fadeOut(),
|
|
||||||
) {
|
|
||||||
ExtendedFloatingActionButton(
|
|
||||||
text = {
|
|
||||||
val id = if (chapters.any { it.chapter.read }) {
|
|
||||||
R.string.action_resume
|
|
||||||
} else {
|
|
||||||
R.string.action_start
|
|
||||||
}
|
|
||||||
Text(text = stringResource(id))
|
|
||||||
},
|
|
||||||
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
|
|
||||||
onClick = onContinueReading,
|
|
||||||
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) { contentPadding ->
|
|
||||||
val withNavBarContentPadding = contentPadding +
|
|
||||||
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
|
||||||
VerticalFastScroller(
|
VerticalFastScroller(
|
||||||
listState = chapterListState,
|
listState = chapterListState,
|
||||||
thumbAllowed = { scrollBehavior.state.offset == scrollBehavior.state.offsetLimit },
|
topContentPadding = topPadding,
|
||||||
topContentPadding = withNavBarContentPadding.calculateTopPadding(),
|
endContentPadding = noTopContentPadding.calculateEndPadding(layoutDirection),
|
||||||
endContentPadding = withNavBarContentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxHeight(),
|
modifier = Modifier.fillMaxHeight(),
|
||||||
state = chapterListState,
|
state = chapterListState,
|
||||||
contentPadding = withNavBarContentPadding,
|
contentPadding = noTopContentPadding,
|
||||||
) {
|
) {
|
||||||
|
item(contentType = "info_box") {
|
||||||
|
MangaInfoBox(
|
||||||
|
windowWidthSizeClass = WindowWidthSizeClass.Compact,
|
||||||
|
appBarPadding = topPadding,
|
||||||
|
title = state.manga.title,
|
||||||
|
author = state.manga.author,
|
||||||
|
artist = state.manga.artist,
|
||||||
|
sourceName = remember { state.source.getNameForMangaInfo() },
|
||||||
|
isStubSource = remember { state.source is SourceManager.StubSource },
|
||||||
|
coverDataProvider = { state.manga },
|
||||||
|
status = state.manga.status,
|
||||||
|
onCoverClick = onCoverClicked,
|
||||||
|
doSearch = onSearch,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item(contentType = "action_row") {
|
||||||
|
MangaActionRow(
|
||||||
|
favorite = state.manga.favorite,
|
||||||
|
trackingCount = state.trackingCount,
|
||||||
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
|
onWebViewClicked = onWebViewClicked,
|
||||||
|
onTrackingClicked = onTrackingClicked,
|
||||||
|
onEditCategory = onEditCategoryClicked,
|
||||||
|
// SY -->
|
||||||
|
onMergeClicked = onMergeClicked,
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
if (metadataSource != null) {
|
||||||
|
item(contentType = "metadata_info") {
|
||||||
|
metadataSource.DescriptionComposable(
|
||||||
|
state = state,
|
||||||
|
openMetadataViewer = onMetadataViewerClicked,
|
||||||
|
search = { onSearch(it, false) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
|
item(contentType = "desc") {
|
||||||
|
ExpandableMangaDescription(
|
||||||
|
defaultExpandState = state.isFromSource,
|
||||||
|
description = state.manga.description,
|
||||||
|
tagsProvider = { state.manga.genre },
|
||||||
|
onTagClicked = onTagClicked,
|
||||||
|
// SY -->
|
||||||
|
doSearch = onSearch,
|
||||||
|
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
|
||||||
|
SearchMetadataChips(state.meta, state.source, state.manga.genre)
|
||||||
|
},
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
if (!state.showRecommendationsInOverflow || state.showMergeWithAnother) {
|
||||||
|
item(contentType = "info_buttons") {
|
||||||
|
MangaInfoButtons(
|
||||||
|
showRecommendsButton = !state.showRecommendationsInOverflow,
|
||||||
|
showMergeWithAnotherButton = state.showMergeWithAnother,
|
||||||
|
onRecommendClicked = onRecommendClicked,
|
||||||
|
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
|
item(contentType = "header") {
|
||||||
|
ChapterHeader(
|
||||||
|
chapterCount = chapters.size,
|
||||||
|
isChapterFiltered = state.manga.chaptersFiltered(),
|
||||||
|
onFilterButtonClicked = onFilterButtonClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
sharedChapterItems(
|
sharedChapterItems(
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
state = state,
|
state = state,
|
||||||
@ -462,6 +494,10 @@ fun MangaScreenLargeImpl(
|
|||||||
val layoutDirection = LocalLayoutDirection.current
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
val metadataSource = remember(state.source.id) { state.source.getMainSource<MetadataSource<*, *>>() }
|
||||||
|
// SY <--
|
||||||
|
|
||||||
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||||
val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) }
|
val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) }
|
||||||
SwipeRefresh(
|
SwipeRefresh(
|
||||||
@ -571,45 +607,66 @@ fun MangaScreenLargeImpl(
|
|||||||
Row {
|
Row {
|
||||||
val withNavBarContentPadding = contentPadding +
|
val withNavBarContentPadding = contentPadding +
|
||||||
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
||||||
MangaInfoHeader(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(bottom = withNavBarContentPadding.calculateBottomPadding()),
|
.padding(bottom = withNavBarContentPadding.calculateBottomPadding()),
|
||||||
windowWidthSizeClass = WindowWidthSizeClass.Expanded,
|
) {
|
||||||
appBarPadding = contentPadding.calculateTopPadding(),
|
MangaInfoBox(
|
||||||
title = state.manga.title,
|
windowWidthSizeClass = windowWidthSizeClass,
|
||||||
author = state.manga.author,
|
appBarPadding = contentPadding.calculateTopPadding(),
|
||||||
artist = state.manga.artist,
|
title = state.manga.title,
|
||||||
description = state.manga.description,
|
author = state.manga.author,
|
||||||
tagsProvider = { state.manga.genre },
|
artist = state.manga.artist,
|
||||||
sourceName = remember { state.source.getNameForMangaInfo() },
|
sourceName = remember { state.source.getNameForMangaInfo() },
|
||||||
isStubSource = remember { state.source is SourceManager.StubSource },
|
isStubSource = remember { state.source is SourceManager.StubSource },
|
||||||
coverDataProvider = { state.manga },
|
coverDataProvider = { state.manga },
|
||||||
favorite = state.manga.favorite,
|
status = state.manga.status,
|
||||||
status = state.manga.status,
|
onCoverClick = onCoverClicked,
|
||||||
trackingCount = state.trackingCount,
|
doSearch = onSearch,
|
||||||
fromSource = state.isFromSource,
|
)
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
MangaActionRow(
|
||||||
onWebViewClicked = onWebViewClicked,
|
favorite = state.manga.favorite,
|
||||||
onTrackingClicked = onTrackingClicked,
|
trackingCount = state.trackingCount,
|
||||||
onMergeClicked = onMergeClicked,
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
onTagClicked = onTagClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
onEditCategory = onEditCategoryClicked,
|
onTrackingClicked = onTrackingClicked,
|
||||||
onCoverClick = onCoverClicked,
|
onEditCategory = onEditCategoryClicked,
|
||||||
doSearch = onSearch,
|
// SY -->
|
||||||
showRecommendsInOverflow = state.showRecommendationsInOverflow,
|
onMergeClicked = onMergeClicked,
|
||||||
showMergeWithAnother = state.showMergeWithAnother,
|
// SY <--
|
||||||
onRecommendClicked = onRecommendClicked,
|
)
|
||||||
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
// SY -->
|
||||||
mangaMetadataHeader = getDescriptionComposable(
|
metadataSource?.DescriptionComposable(
|
||||||
source = remember { state.source.getMainSource<MetadataSource<*, *>>() },
|
|
||||||
state = state,
|
state = state,
|
||||||
openMetadataViewer = onMetadataViewerClicked,
|
openMetadataViewer = onMetadataViewerClicked,
|
||||||
search = { onSearch(it, false) },
|
search = { onSearch(it, false) },
|
||||||
),
|
)
|
||||||
searchMetadataChips = remember(state) { SearchMetadataChips(state.meta, state.source, state.manga.genre) },
|
// SY <--
|
||||||
)
|
ExpandableMangaDescription(
|
||||||
|
defaultExpandState = true,
|
||||||
|
description = state.manga.description,
|
||||||
|
tagsProvider = { state.manga.genre },
|
||||||
|
onTagClicked = onTagClicked,
|
||||||
|
// SY -->
|
||||||
|
doSearch = onSearch,
|
||||||
|
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
|
||||||
|
SearchMetadataChips(state.meta, state.source, state.manga.genre)
|
||||||
|
},
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
// SY -->
|
||||||
|
if (!state.showRecommendationsInOverflow || state.showMergeWithAnother) {
|
||||||
|
MangaInfoButtons(
|
||||||
|
showRecommendsButton = !state.showRecommendationsInOverflow,
|
||||||
|
showMergeWithAnotherButton = state.showMergeWithAnother,
|
||||||
|
onRecommendClicked = onRecommendClicked,
|
||||||
|
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
}
|
||||||
|
|
||||||
val chaptersWeight = if (windowWidthSizeClass == WindowWidthSizeClass.Medium) 1f else 2f
|
val chaptersWeight = if (windowWidthSizeClass == WindowWidthSizeClass.Medium) 1f else 2f
|
||||||
VerticalFastScroller(
|
VerticalFastScroller(
|
||||||
@ -727,7 +784,7 @@ private fun LazyListScope.sharedChapterItems(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val lastPageRead = remember(chapter.lastPageRead) {
|
val lastPageRead = remember(chapter.lastPageRead) {
|
||||||
chapter.lastPageRead.takeIf { !chapter.read && it > 0 }
|
chapter.lastPageRead.takeIf { /* SY --> */(!chapter.read || state.alwaysShowPageProgress)/* SY <-- */ && it > 0 }
|
||||||
}
|
}
|
||||||
val scanlator = remember(chapter.scanlator) { chapter.scanlator.takeIf { !it.isNullOrBlank() } }
|
val scanlator = remember(chapter.scanlator) { chapter.scanlator.takeIf { !it.isNullOrBlank() } }
|
||||||
|
|
||||||
@ -840,19 +897,3 @@ private fun onChapterItemClick(
|
|||||||
else -> onChapterClicked(chapterItem.chapter)
|
else -> onChapterClicked(chapterItem.chapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
@ReadOnlyComposable
|
|
||||||
@Stable
|
|
||||||
private fun getDescriptionComposable(
|
|
||||||
source: MetadataSource<*, *>?,
|
|
||||||
state: MangaScreenState.Success,
|
|
||||||
openMetadataViewer: () -> Unit,
|
|
||||||
search: (String) -> Unit,
|
|
||||||
): (@Composable () -> Unit)? {
|
|
||||||
return if (source != null) {
|
|
||||||
@Composable {
|
|
||||||
source.DescriptionComposable(state = state, openMetadataViewer = openMetadataViewer, search = search)
|
|
||||||
}
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
|
@ -83,219 +83,221 @@ import kotlin.math.roundToInt
|
|||||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaInfoHeader(
|
fun MangaInfoBox(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
windowWidthSizeClass: WindowWidthSizeClass,
|
windowWidthSizeClass: WindowWidthSizeClass,
|
||||||
appBarPadding: Dp,
|
appBarPadding: Dp,
|
||||||
title: String,
|
title: String,
|
||||||
author: String?,
|
author: String?,
|
||||||
artist: String?,
|
artist: String?,
|
||||||
description: String?,
|
|
||||||
tagsProvider: () -> List<String>?,
|
|
||||||
sourceName: String,
|
sourceName: String,
|
||||||
isStubSource: Boolean,
|
isStubSource: Boolean,
|
||||||
coverDataProvider: () -> Manga,
|
coverDataProvider: () -> Manga,
|
||||||
favorite: Boolean,
|
|
||||||
status: Long,
|
status: Long,
|
||||||
|
onCoverClick: () -> Unit,
|
||||||
|
doSearch: (query: String, global: Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
// 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
|
||||||
|
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||||
|
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
|
||||||
|
MangaAndSourceTitlesSmall(
|
||||||
|
appBarPadding = appBarPadding,
|
||||||
|
coverDataProvider = coverDataProvider,
|
||||||
|
onCoverClick = onCoverClick,
|
||||||
|
title = title,
|
||||||
|
context = LocalContext.current,
|
||||||
|
doSearch = doSearch,
|
||||||
|
author = author,
|
||||||
|
artist = artist,
|
||||||
|
status = status,
|
||||||
|
sourceName = sourceName,
|
||||||
|
isStubSource = isStubSource,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
MangaAndSourceTitlesLarge(
|
||||||
|
appBarPadding = appBarPadding,
|
||||||
|
coverDataProvider = coverDataProvider,
|
||||||
|
onCoverClick = onCoverClick,
|
||||||
|
title = title,
|
||||||
|
context = LocalContext.current,
|
||||||
|
doSearch = doSearch,
|
||||||
|
author = author,
|
||||||
|
artist = artist,
|
||||||
|
status = status,
|
||||||
|
sourceName = sourceName,
|
||||||
|
isStubSource = isStubSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MangaActionRow(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
favorite: Boolean,
|
||||||
trackingCount: Int,
|
trackingCount: Int,
|
||||||
fromSource: Boolean,
|
|
||||||
onAddToLibraryClicked: () -> Unit,
|
onAddToLibraryClicked: () -> Unit,
|
||||||
onWebViewClicked: (() -> Unit)?,
|
onWebViewClicked: (() -> Unit)?,
|
||||||
onTrackingClicked: (() -> Unit)?,
|
onTrackingClicked: (() -> Unit)?,
|
||||||
onMergeClicked: () -> Unit,
|
|
||||||
onTagClicked: (String) -> Unit,
|
|
||||||
onEditCategory: (() -> Unit)?,
|
onEditCategory: (() -> Unit)?,
|
||||||
onCoverClick: () -> Unit,
|
// SY -->
|
||||||
doSearch: (query: String, global: Boolean) -> Unit,
|
onMergeClicked: () -> Unit,
|
||||||
onRecommendClicked: () -> Unit,
|
// SY <--
|
||||||
showRecommendsInOverflow: Boolean,
|
) {
|
||||||
showMergeWithAnother: Boolean,
|
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||||
onMergeWithAnotherClicked: () -> Unit,
|
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||||
mangaMetadataHeader: (@Composable () -> Unit)?,
|
MangaActionButton(
|
||||||
|
title = if (favorite) {
|
||||||
|
stringResource(R.string.in_library)
|
||||||
|
} else {
|
||||||
|
stringResource(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(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(R.string.action_web_view),
|
||||||
|
icon = Icons.Default.Public,
|
||||||
|
color = defaultActionButtonColor,
|
||||||
|
onClick = onWebViewClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// SY -->
|
||||||
|
MangaActionButton(
|
||||||
|
title = stringResource(R.string.merge),
|
||||||
|
icon = Icons.Outlined.CallMerge,
|
||||||
|
color = defaultActionButtonColor,
|
||||||
|
onClick = onMergeClicked,
|
||||||
|
)
|
||||||
|
// SY <--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExpandableMangaDescription(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
defaultExpandState: Boolean,
|
||||||
|
description: String?,
|
||||||
|
tagsProvider: () -> List<String>?,
|
||||||
|
onTagClicked: (String) -> Unit,
|
||||||
|
// SY -->
|
||||||
searchMetadataChips: SearchMetadataChips?,
|
searchMetadataChips: SearchMetadataChips?,
|
||||||
|
doSearch: (query: String, global: Boolean) -> Unit,
|
||||||
|
// SY <--
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Box {
|
val (expanded, onExpanded) = rememberSaveable {
|
||||||
// Backdrop
|
mutableStateOf(defaultExpandState)
|
||||||
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
|
|
||||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
val desc =
|
||||||
// Action buttons
|
description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder)
|
||||||
Row(modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
val trimmedDescription = remember(desc) {
|
||||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
desc
|
||||||
MangaActionButton(
|
.replace(whitespaceLineRegex, "\n")
|
||||||
title = if (favorite) {
|
.trimEnd()
|
||||||
stringResource(R.string.in_library)
|
|
||||||
} else {
|
|
||||||
stringResource(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(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(R.string.action_web_view),
|
|
||||||
icon = Icons.Default.Public,
|
|
||||||
color = defaultActionButtonColor,
|
|
||||||
onClick = onWebViewClicked,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
MangaActionButton(
|
|
||||||
title = stringResource(R.string.merge),
|
|
||||||
icon = Icons.Outlined.CallMerge,
|
|
||||||
color = defaultActionButtonColor,
|
|
||||||
onClick = onMergeClicked,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
MangaSummary(
|
||||||
// SY --> Manga metadata
|
expandedDescription = desc,
|
||||||
mangaMetadataHeader?.invoke()
|
shrunkDescription = trimmedDescription,
|
||||||
// SY <--
|
expanded = expanded,
|
||||||
|
modifier = Modifier
|
||||||
// Expandable description-tags
|
.padding(top = 8.dp)
|
||||||
Column {
|
.padding(horizontal = 16.dp)
|
||||||
val (expanded, onExpanded) = rememberSaveable {
|
.clickableNoIndication(
|
||||||
mutableStateOf(fromSource || windowWidthSizeClass != WindowWidthSizeClass.Compact)
|
onLongClick = { context.copyToClipboard(desc, desc) },
|
||||||
}
|
onClick = { onExpanded(!expanded) },
|
||||||
val desc =
|
),
|
||||||
description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder)
|
)
|
||||||
val trimmedDescription = remember(desc) {
|
val tags = tagsProvider()
|
||||||
desc
|
if (!tags.isNullOrEmpty()) {
|
||||||
.replace(whitespaceLineRegex, "\n")
|
Box(
|
||||||
.trimEnd()
|
|
||||||
}
|
|
||||||
MangaSummary(
|
|
||||||
expandedDescription = desc,
|
|
||||||
shrunkDescription = trimmedDescription,
|
|
||||||
expanded = expanded,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(vertical = 12.dp)
|
||||||
.clickableNoIndication(
|
.animateContentSize(),
|
||||||
onLongClick = { context.copyToClipboard(desc, desc) },
|
) {
|
||||||
onClick = { onExpanded(!expanded) },
|
if (expanded) {
|
||||||
),
|
// SY -->
|
||||||
)
|
if (searchMetadataChips != null) {
|
||||||
val tags = tagsProvider()
|
NamespaceTags(
|
||||||
if (!tags.isNullOrEmpty()) {
|
tags = searchMetadataChips,
|
||||||
Box(
|
onClick = onTagClicked,
|
||||||
modifier = Modifier
|
onLongClick = { doSearch(it, true) },
|
||||||
.padding(top = 8.dp)
|
)
|
||||||
.padding(vertical = 12.dp)
|
|
||||||
.animateContentSize(),
|
|
||||||
) {
|
|
||||||
if (expanded) {
|
|
||||||
if (searchMetadataChips != null) {
|
|
||||||
NamespaceTags(
|
|
||||||
tags = searchMetadataChips,
|
|
||||||
onClick = onTagClicked,
|
|
||||||
onLongClick = { doSearch(it, true) },
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
FlowRow(
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
mainAxisSpacing = 4.dp,
|
|
||||||
crossAxisSpacing = 8.dp,
|
|
||||||
) {
|
|
||||||
tags.forEach {
|
|
||||||
TagsChip(
|
|
||||||
text = it,
|
|
||||||
onClick = { onTagClicked(it) },
|
|
||||||
onLongClick = { doSearch(it, true) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
LazyRow(
|
// SY <--
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
FlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
mainAxisSpacing = 4.dp,
|
||||||
|
crossAxisSpacing = 8.dp,
|
||||||
) {
|
) {
|
||||||
items(items = tags) {
|
tags.forEach {
|
||||||
TagsChip(
|
TagsChip(
|
||||||
text = it,
|
text = it,
|
||||||
onClick = { onTagClicked(it) },
|
onClick = { onTagClicked(it) },
|
||||||
|
// SY -->
|
||||||
onLongClick = { doSearch(it, true) },
|
onLongClick = { doSearch(it, true) },
|
||||||
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
LazyRow(
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
items(items = tags) {
|
||||||
|
TagsChip(
|
||||||
|
text = it,
|
||||||
|
onClick = { onTagClicked(it) },
|
||||||
|
// SY -->
|
||||||
|
onLongClick = { doSearch(it, true) },
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SY -->
|
|
||||||
MangaInfoButtons(
|
|
||||||
showRecommendsButton = !showRecommendsInOverflow,
|
|
||||||
showMergeWithAnotherButton = showMergeWithAnother,
|
|
||||||
onRecommendClicked = onRecommendClicked,
|
|
||||||
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
|
||||||
)
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,12 +54,14 @@ fun MangaSmallAppBar(
|
|||||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
// SY -->
|
||||||
showEditInfo: Boolean,
|
showEditInfo: Boolean,
|
||||||
onEditInfoClicked: () -> Unit,
|
onEditInfoClicked: () -> Unit,
|
||||||
showRecommends: Boolean,
|
showRecommends: Boolean,
|
||||||
onRecommendClicked: () -> Unit,
|
onRecommendClicked: () -> Unit,
|
||||||
showMergeSettings: Boolean,
|
showMergeSettings: Boolean,
|
||||||
onMergedSettingsClicked: () -> Unit,
|
onMergedSettingsClicked: () -> Unit,
|
||||||
|
// SY <--
|
||||||
// For action mode
|
// For action mode
|
||||||
actionModeCounter: Int,
|
actionModeCounter: Int,
|
||||||
onSelectAll: () -> Unit,
|
onSelectAll: () -> Unit,
|
||||||
|
@ -1,167 +0,0 @@
|
|||||||
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)?,
|
|
||||||
onEditInfoClicked: () -> Unit,
|
|
||||||
onRecommendClicked: () -> Unit,
|
|
||||||
showRecommendsInOverflow: Boolean,
|
|
||||||
showMergedSettings: Boolean,
|
|
||||||
onMergedSettingsClicked: () -> Unit,
|
|
||||||
onMergeClicked: () -> Unit,
|
|
||||||
showMergeWithAnother: Boolean,
|
|
||||||
onMergeWithAnotherClicked: () -> Unit,
|
|
||||||
doGlobalSearch: (query: String, global: Boolean) -> Unit,
|
|
||||||
mangaMetadataHeader: (@Composable () -> Unit)?,
|
|
||||||
searchMetadataChips: SearchMetadataChips?,
|
|
||||||
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,
|
|
||||||
onMergeClicked = onMergeClicked,
|
|
||||||
onTagClicked = onTagClicked,
|
|
||||||
onEditCategory = onEditCategoryClicked,
|
|
||||||
onCoverClick = onCoverClick,
|
|
||||||
doSearch = doGlobalSearch,
|
|
||||||
showRecommendsInOverflow = showRecommendsInOverflow,
|
|
||||||
showMergeWithAnother = showMergeWithAnother,
|
|
||||||
onRecommendClicked = onRecommendClicked,
|
|
||||||
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
|
||||||
mangaMetadataHeader = mangaMetadataHeader,
|
|
||||||
searchMetadataChips = searchMetadataChips,
|
|
||||||
)
|
|
||||||
|
|
||||||
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,
|
|
||||||
// SY -->
|
|
||||||
showEditInfo = favorite,
|
|
||||||
onEditInfoClicked = onEditInfoClicked,
|
|
||||||
showRecommends = showRecommendsInOverflow,
|
|
||||||
onRecommendClicked = onRecommendClicked,
|
|
||||||
showMergeSettings = showMergedSettings,
|
|
||||||
onMergedSettingsClicked = onMergedSettingsClicked,
|
|
||||||
// SY <--
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -306,6 +306,7 @@ class MangaPresenter(
|
|||||||
mergedData = mergedData,
|
mergedData = mergedData,
|
||||||
showRecommendationsInOverflow = preferences.recommendsInOverflow().get(),
|
showRecommendationsInOverflow = preferences.recommendsInOverflow().get(),
|
||||||
showMergeWithAnother = smartSearched,
|
showMergeWithAnother = smartSearched,
|
||||||
|
alwaysShowPageProgress = preferences.preserveReadingPosition().get() && manga.isEhBasedManga(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1333,6 +1334,7 @@ sealed class MangaScreenState {
|
|||||||
val mergedData: MergedMangaData?,
|
val mergedData: MergedMangaData?,
|
||||||
val showRecommendationsInOverflow: Boolean,
|
val showRecommendationsInOverflow: Boolean,
|
||||||
val showMergeWithAnother: Boolean,
|
val showMergeWithAnother: Boolean,
|
||||||
|
val alwaysShowPageProgress: Boolean,
|
||||||
// SY <--
|
// SY <--
|
||||||
) : MangaScreenState() {
|
) : MangaScreenState() {
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user