diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index 9a3c7271b..4e6b8e107 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -2,15 +2,11 @@ package eu.kanade.presentation.manga import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn 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.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets @@ -36,12 +32,10 @@ import androidx.compose.material3.Icon import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.rememberTopAppBarScrollState import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.SnapshotStateList @@ -49,7 +43,6 @@ import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext 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.VerticalFastScroller import eu.kanade.presentation.manga.components.ChapterHeader +import eu.kanade.presentation.manga.components.ExpandableMangaDescription +import eu.kanade.presentation.manga.components.MangaActionRow import eu.kanade.presentation.manga.components.MangaBottomActionMenu import eu.kanade.presentation.manga.components.MangaChapterListItem -import eu.kanade.presentation.manga.components.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.MangaTopAppBar import eu.kanade.presentation.manga.components.SearchMetadataChips -import eu.kanade.presentation.util.ExitUntilCollapsedScrollBehavior import eu.kanade.presentation.util.isScrolledToEnd import eu.kanade.presentation.util.isScrollingUp 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 exh.source.MERGED_SOURCE_ID import exh.source.getMainSource -import kotlinx.coroutines.runBlocking import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.Date @@ -238,175 +231,214 @@ private fun MangaScreenSmallImpl( onMultiDeleteClicked: (List<Chapter>) -> Unit, ) { val layoutDirection = LocalLayoutDirection.current - val decayAnimationSpec = rememberSplineBasedDecay<Float>() - val scrollBehavior = ExitUntilCollapsedScrollBehavior(rememberTopAppBarScrollState(), decayAnimationSpec) 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 (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(1) } - SwipeRefresh( - state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter), - onRefresh = onRefresh, - indicatorPadding = PaddingValues( - start = insetPadding.calculateStartPadding(layoutDirection), - top = with(LocalDensity.current) { topBarHeight.toDp() }, - end = insetPadding.calculateEndPadding(layoutDirection), - ), - indicator = { s, trigger -> - SwipeRefreshIndicator( - state = s, - refreshTriggerDistance = trigger, + val chapters = remember(state) { state.processedChapters.toList() } + val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() } + val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list + // SY --> + val metadataSource = remember(state.source.id) { state.source.getMainSource<MetadataSource<*, *>>() } + // SY <-- + + val internalOnBackPressed = { + if (selected.isNotEmpty()) { + selected.clear() + } else { + onBackClicked() + } + } + 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) + }, ) }, - ) { - val chapters = remember(state) { state.processedChapters.toList() } - val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() } - val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list - - val internalOnBackPressed = { - if (selected.isNotEmpty()) { - selected.clear() - } else { - onBackClicked() - } - } - BackHandler(onBack = internalOnBackPressed) - - Scaffold( - modifier = Modifier - .nestedScroll(scrollBehavior.nestedScrollConnection) - .padding(insetPadding), - topBar = { - MangaTopAppBar( + 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 - .scrollable( - 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 - val newOffset = - (scrollBehavior.state.offset + it).coerceIn(scrollBehavior.state.offsetLimit, 0f) - consumed = newOffset - scrollBehavior.state.offset - scrollBehavior.state.offset = newOffset - } - consumed - }, - orientation = Orientation.Vertical, - interactionSource = chapterListState.interactionSource as MutableInteractionSource, - ), - title = state.manga.title, - author = state.manga.author, - artist = state.manga.artist, - description = state.manga.description, - tagsProvider = { state.manga.genre }, - 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, + .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()), + ) + } + }, + ) { contentPadding -> + val noTopContentPadding = PaddingValues( + start = contentPadding.calculateStartPadding(layoutDirection), + end = contentPadding.calculateEndPadding(layoutDirection), + bottom = contentPadding.calculateBottomPadding(), + ) + WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() + val topPadding = contentPadding.calculateTopPadding() + + SwipeRefresh( + state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter), + onRefresh = onRefresh, + indicatorPadding = contentPadding, + indicator = { s, trigger -> + SwipeRefreshIndicator( + state = s, + refreshTriggerDistance = trigger, ) }, - 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( listState = chapterListState, - thumbAllowed = { scrollBehavior.state.offset == scrollBehavior.state.offsetLimit }, - topContentPadding = withNavBarContentPadding.calculateTopPadding(), - endContentPadding = withNavBarContentPadding.calculateEndPadding(LocalLayoutDirection.current), + topContentPadding = topPadding, + endContentPadding = noTopContentPadding.calculateEndPadding(layoutDirection), ) { LazyColumn( modifier = Modifier.fillMaxHeight(), 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( chapters = chapters, state = state, @@ -462,6 +494,10 @@ fun MangaScreenLargeImpl( val layoutDirection = LocalLayoutDirection.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 (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) } SwipeRefresh( @@ -571,45 +607,66 @@ fun MangaScreenLargeImpl( Row { val withNavBarContentPadding = contentPadding + WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() - MangaInfoHeader( + Column( modifier = Modifier .weight(1f) .verticalScroll(rememberScrollState()) .padding(bottom = withNavBarContentPadding.calculateBottomPadding()), - windowWidthSizeClass = WindowWidthSizeClass.Expanded, - appBarPadding = contentPadding.calculateTopPadding(), - title = state.manga.title, - author = state.manga.author, - artist = state.manga.artist, - description = state.manga.description, - tagsProvider = { state.manga.genre }, - sourceName = remember { state.source.getNameForMangaInfo() }, - isStubSource = remember { state.source is SourceManager.StubSource }, - coverDataProvider = { state.manga }, - favorite = state.manga.favorite, - status = state.manga.status, - trackingCount = state.trackingCount, - fromSource = state.isFromSource, - onAddToLibraryClicked = onAddToLibraryClicked, - onWebViewClicked = onWebViewClicked, - onTrackingClicked = onTrackingClicked, - onMergeClicked = onMergeClicked, - onTagClicked = onTagClicked, - onEditCategory = onEditCategoryClicked, - onCoverClick = onCoverClicked, - doSearch = onSearch, - showRecommendsInOverflow = state.showRecommendationsInOverflow, - showMergeWithAnother = state.showMergeWithAnother, - onRecommendClicked = onRecommendClicked, - onMergeWithAnotherClicked = onMergeWithAnotherClicked, - mangaMetadataHeader = getDescriptionComposable( - source = remember { state.source.getMainSource<MetadataSource<*, *>>() }, + ) { + MangaInfoBox( + windowWidthSizeClass = windowWidthSizeClass, + appBarPadding = contentPadding.calculateTopPadding(), + 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, + ) + MangaActionRow( + favorite = state.manga.favorite, + trackingCount = state.trackingCount, + onAddToLibraryClicked = onAddToLibraryClicked, + onWebViewClicked = onWebViewClicked, + onTrackingClicked = onTrackingClicked, + onEditCategory = onEditCategoryClicked, + // SY --> + onMergeClicked = onMergeClicked, + // SY <-- + ) + // SY --> + metadataSource?.DescriptionComposable( state = state, openMetadataViewer = onMetadataViewerClicked, 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 VerticalFastScroller( @@ -727,7 +784,7 @@ private fun LazyListScope.sharedChapterItems( } } 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() } } @@ -840,19 +897,3 @@ private fun onChapterItemClick( 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 -} diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index b977e5524..edb542fe5 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -83,219 +83,221 @@ import kotlin.math.roundToInt private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)) @Composable -fun MangaInfoHeader( +fun MangaInfoBox( 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, + 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, - fromSource: Boolean, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onTrackingClicked: (() -> Unit)?, - onMergeClicked: () -> Unit, - onTagClicked: (String) -> Unit, onEditCategory: (() -> Unit)?, - onCoverClick: () -> Unit, - doSearch: (query: String, global: Boolean) -> Unit, - onRecommendClicked: () -> Unit, - showRecommendsInOverflow: Boolean, - showMergeWithAnother: Boolean, - onMergeWithAnotherClicked: () -> Unit, - mangaMetadataHeader: (@Composable () -> Unit)?, + // SY --> + onMergeClicked: () -> Unit, + // SY <-- +) { + 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(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?, + doSearch: (query: String, global: Boolean) -> Unit, + // SY <-- ) { 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 - 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 (expanded, onExpanded) = rememberSaveable { + mutableStateOf(defaultExpandState) } - - // 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(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, - ) + val desc = + description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder) + val trimmedDescription = remember(desc) { + desc + .replace(whitespaceLineRegex, "\n") + .trimEnd() } - - // SY --> Manga metadata - mangaMetadataHeader?.invoke() - // SY <-- - - // Expandable description-tags - Column { - val (expanded, onExpanded) = rememberSaveable { - mutableStateOf(fromSource || windowWidthSizeClass != WindowWidthSizeClass.Compact) - } - val desc = - description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder) - val trimmedDescription = remember(desc) { - desc - .replace(whitespaceLineRegex, "\n") - .trimEnd() - } - MangaSummary( - expandedDescription = desc, - shrunkDescription = trimmedDescription, - expanded = expanded, + MangaSummary( + expandedDescription = desc, + shrunkDescription = trimmedDescription, + expanded = expanded, + modifier = Modifier + .padding(top = 8.dp) + .padding(horizontal = 16.dp) + .clickableNoIndication( + onLongClick = { context.copyToClipboard(desc, desc) }, + onClick = { onExpanded(!expanded) }, + ), + ) + val tags = tagsProvider() + if (!tags.isNullOrEmpty()) { + Box( modifier = Modifier .padding(top = 8.dp) - .padding(horizontal = 16.dp) - .clickableNoIndication( - onLongClick = { context.copyToClipboard(desc, desc) }, - onClick = { onExpanded(!expanded) }, - ), - ) - val tags = tagsProvider() - if (!tags.isNullOrEmpty()) { - Box( - modifier = Modifier - .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) }, - ) - } - } - } + .padding(vertical = 12.dp) + .animateContentSize(), + ) { + if (expanded) { + // SY --> + if (searchMetadataChips != null) { + NamespaceTags( + tags = searchMetadataChips, + onClick = onTagClicked, + onLongClick = { doSearch(it, true) }, + ) } else { - LazyRow( - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), + // SY <-- + FlowRow( + modifier = Modifier.padding(horizontal = 16.dp), + mainAxisSpacing = 4.dp, + crossAxisSpacing = 8.dp, ) { - items(items = tags) { + tags.forEach { TagsChip( text = it, onClick = { onTagClicked(it) }, + // SY --> 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 <-- } } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaSmallAppBar.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaSmallAppBar.kt index 78c7e18ff..fe2531d9c 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaSmallAppBar.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaSmallAppBar.kt @@ -54,12 +54,14 @@ fun MangaSmallAppBar( onDownloadClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, + // SY --> showEditInfo: Boolean, onEditInfoClicked: () -> Unit, showRecommends: Boolean, onRecommendClicked: () -> Unit, showMergeSettings: Boolean, onMergedSettingsClicked: () -> Unit, + // SY <-- // For action mode actionModeCounter: Int, onSelectAll: () -> Unit, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaTopAppBar.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaTopAppBar.kt deleted file mode 100644 index e2e304389..000000000 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaTopAppBar.kt +++ /dev/null @@ -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 - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index cb29b1f83..e5cc0a775 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -306,6 +306,7 @@ class MangaPresenter( mergedData = mergedData, showRecommendationsInOverflow = preferences.recommendsInOverflow().get(), showMergeWithAnother = smartSearched, + alwaysShowPageProgress = preferences.preserveReadingPosition().get() && manga.isEhBasedManga(), ) } @@ -1333,6 +1334,7 @@ sealed class MangaScreenState { val mergedData: MergedMangaData?, val showRecommendationsInOverflow: Boolean, val showMergeWithAnother: Boolean, + val alwaysShowPageProgress: Boolean, // SY <-- ) : MangaScreenState() {