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.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Icon import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastMap import eu.kanade.presentation.components.relativeDateText import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.manga.components.ChapterHeader import eu.kanade.presentation.manga.components.ExpandableMangaDescription import eu.kanade.presentation.manga.components.MangaActionRow import eu.kanade.presentation.manga.components.MangaBottomActionMenu import eu.kanade.presentation.manga.components.MangaChapterListItem import eu.kanade.presentation.manga.components.MangaInfoBox import eu.kanade.presentation.manga.components.MangaToolbar import eu.kanade.presentation.manga.components.MissingChapterCountListItem import eu.kanade.presentation.util.formatChapterNumber import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.source.getNameForMangaInfo import eu.kanade.tachiyomi.ui.manga.ChapterList import eu.kanade.tachiyomi.ui.manga.MangaScreenModel import eu.kanade.tachiyomi.util.system.copyToClipboard import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.service.missingChaptersCount import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.source.model.StubSource import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.TwoPanelBox import tachiyomi.presentation.core.components.VerticalFastScroller import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.isScrollingUp import tachiyomi.source.local.isLocal import java.time.Instant @Composable fun MangaScreen( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, nextUpdate: Instant?, isTabletUi: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: () -> Unit, // For tags menu onTagSearch: (String) -> Unit, onFilterButtonClicked: () -> Unit, onRefresh: () -> Unit, onContinueReading: () -> Unit, onSearch: (query: String, global: Boolean) -> Unit, // For cover dialog onCoverClicked: () -> Unit, // For top action menu onShareClicked: (() -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onEditFetchIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List) -> Unit, // For chapter swipe onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) { val context = LocalContext.current val onCopyTagToClipboard: (tag: String) -> Unit = { if (it.isNotEmpty()) { context.copyToClipboard(it, it) } } if (!isTabletUi) { MangaScreenSmallImpl( state = state, snackbarHostState = snackbarHostState, nextUpdate = nextUpdate, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, onBackClicked = onBackClicked, onChapterClicked = onChapterClicked, onDownloadChapter = onDownloadChapter, onAddToLibraryClicked = onAddToLibraryClicked, onWebViewClicked = onWebViewClicked, onWebViewLongClicked = onWebViewLongClicked, onTrackingClicked = onTrackingClicked, onTagSearch = onTagSearch, onCopyTagToClipboard = onCopyTagToClipboard, onFilterClicked = onFilterButtonClicked, onRefresh = onRefresh, onContinueReading = onContinueReading, onSearch = onSearch, onCoverClicked = onCoverClicked, onShareClicked = onShareClicked, onDownloadActionClicked = onDownloadActionClicked, onEditCategoryClicked = onEditCategoryClicked, onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, onMultiDeleteClicked = onMultiDeleteClicked, onChapterSwipe = onChapterSwipe, onChapterSelected = onChapterSelected, onAllChapterSelected = onAllChapterSelected, onInvertSelection = onInvertSelection, ) } else { MangaScreenLargeImpl( state = state, snackbarHostState = snackbarHostState, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, nextUpdate = nextUpdate, onBackClicked = onBackClicked, onChapterClicked = onChapterClicked, onDownloadChapter = onDownloadChapter, onAddToLibraryClicked = onAddToLibraryClicked, onWebViewClicked = onWebViewClicked, onWebViewLongClicked = onWebViewLongClicked, onTrackingClicked = onTrackingClicked, onTagSearch = onTagSearch, onCopyTagToClipboard = onCopyTagToClipboard, onFilterButtonClicked = onFilterButtonClicked, onRefresh = onRefresh, onContinueReading = onContinueReading, onSearch = onSearch, onCoverClicked = onCoverClicked, onShareClicked = onShareClicked, onDownloadActionClicked = onDownloadActionClicked, onEditCategoryClicked = onEditCategoryClicked, onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, onMultiDeleteClicked = onMultiDeleteClicked, onChapterSwipe = onChapterSwipe, onChapterSelected = onChapterSelected, onAllChapterSelected = onAllChapterSelected, onInvertSelection = onInvertSelection, ) } } @Composable private fun MangaScreenSmallImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, nextUpdate: Instant?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: () -> Unit, // For tags menu onTagSearch: (String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit, onFilterClicked: () -> Unit, onRefresh: () -> Unit, onContinueReading: () -> Unit, onSearch: (query: String, global: Boolean) -> Unit, // For cover dialog onCoverClicked: () -> Unit, // For top action menu onShareClicked: (() -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List) -> Unit, // For chapter swipe onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) { val chapterListState = rememberLazyListState() val (chapters, listItem, isAnySelected) = remember(state) { Triple( first = state.processedChapters, second = state.chapterListItems, third = state.isAnySelected, ) } val internalOnBackPressed = { if (isAnySelected) { onAllChapterSelected(false) } else { onBackClicked() } } BackHandler(onBack = internalOnBackPressed) Scaffold( topBar = { val selectedChapterCount: Int = remember(chapters) { chapters.count { it.selected } } val isFirstItemVisible by remember { derivedStateOf { chapterListState.firstVisibleItemIndex == 0 } } val isFirstItemScrolled by remember { derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 } } val animatedTitleAlpha by animateFloatAsState( if (!isFirstItemVisible) 1f else 0f, label = "Top Bar Title", ) val animatedBgAlpha by animateFloatAsState( if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f, label = "Top Bar Background", ) MangaToolbar( title = state.manga.title, titleAlphaProvider = { animatedTitleAlpha }, backgroundAlphaProvider = { animatedBgAlpha }, hasFilters = state.filterActive, onBackClicked = internalOnBackPressed, onClickFilter = onFilterClicked, onClickShare = onShareClicked, onClickDownload = onDownloadActionClicked, onClickEditCategory = onEditCategoryClicked, onClickRefresh = onRefresh, onClickMigrate = onMigrateClicked, actionModeCounter = selectedChapterCount, onSelectAll = { onAllChapterSelected(true) }, onInvertSelection = { onInvertSelection() }, ) }, bottomBar = { val selectedChapters = remember(chapters) { chapters.filter { it.selected } } SharedMangaBottomActionMenu( selected = selectedChapters, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, onDownloadChapter = onDownloadChapter, onMultiDeleteClicked = onMultiDeleteClicked, fillFraction = 1f, ) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, floatingActionButton = { val isFABVisible = remember(chapters) { chapters.fastAny { !it.chapter.read } && !isAnySelected } AnimatedVisibility( visible = isFABVisible, enter = fadeIn(), exit = fadeOut(), ) { ExtendedFloatingActionButton( text = { val isReading = remember(state.chapters) { state.chapters.fastAny { it.chapter.read } } Text( text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start), ) }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, onClick = onContinueReading, expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), ) } }, ) { contentPadding -> val topPadding = contentPadding.calculateTopPadding() PullRefresh( refreshing = state.isRefreshingData, onRefresh = onRefresh, enabled = { !isAnySelected }, indicatorPadding = PaddingValues(top = topPadding), ) { val layoutDirection = LocalLayoutDirection.current VerticalFastScroller( listState = chapterListState, topContentPadding = topPadding, endContentPadding = contentPadding.calculateEndPadding(layoutDirection), ) { LazyColumn( modifier = Modifier.fillMaxHeight(), state = chapterListState, contentPadding = PaddingValues( start = contentPadding.calculateStartPadding(layoutDirection), end = contentPadding.calculateEndPadding(layoutDirection), bottom = contentPadding.calculateBottomPadding(), ), ) { item( key = MangaScreenItem.INFO_BOX, contentType = MangaScreenItem.INFO_BOX, ) { MangaInfoBox( isTabletUi = false, appBarPadding = topPadding, title = state.manga.title, author = state.manga.author, artist = state.manga.artist, sourceName = remember { state.source.getNameForMangaInfo() }, isStubSource = remember { state.source is StubSource }, coverDataProvider = { state.manga }, status = state.manga.status, onCoverClick = onCoverClicked, doSearch = onSearch, ) } item( key = MangaScreenItem.ACTION_ROW, contentType = MangaScreenItem.ACTION_ROW, ) { MangaActionRow( favorite = state.manga.favorite, trackingCount = state.trackingCount, nextUpdate = nextUpdate, isUserIntervalMode = state.manga.fetchInterval < 0, onAddToLibraryClicked = onAddToLibraryClicked, onWebViewClicked = onWebViewClicked, onWebViewLongClicked = onWebViewLongClicked, onTrackingClicked = onTrackingClicked, onEditIntervalClicked = onEditIntervalClicked, onEditCategory = onEditCategoryClicked, ) } item( key = MangaScreenItem.DESCRIPTION_WITH_TAG, contentType = MangaScreenItem.DESCRIPTION_WITH_TAG, ) { ExpandableMangaDescription( defaultExpandState = state.isFromSource, description = state.manga.description, tagsProvider = { state.manga.genre }, onTagSearch = onTagSearch, onCopyTagToClipboard = onCopyTagToClipboard, ) } item( key = MangaScreenItem.CHAPTER_HEADER, contentType = MangaScreenItem.CHAPTER_HEADER, ) { val missingChapterCount = remember(chapters) { chapters.map { it.chapter.chapterNumber }.missingChaptersCount() } ChapterHeader( enabled = !isAnySelected, chapterCount = chapters.size, missingChapterCount = missingChapterCount, onClick = onFilterClicked, ) } sharedChapterItems( manga = state.manga, chapters = listItem, isAnyChapterSelected = chapters.fastAny { it.selected }, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, onChapterClicked = onChapterClicked, onDownloadChapter = onDownloadChapter, onChapterSelected = onChapterSelected, onChapterSwipe = onChapterSwipe, ) } } } } } @Composable fun MangaScreenLargeImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, nextUpdate: Instant?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: () -> Unit, // For tags menu onTagSearch: (String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit, onFilterButtonClicked: () -> Unit, onRefresh: () -> Unit, onContinueReading: () -> Unit, onSearch: (query: String, global: Boolean) -> Unit, // For cover dialog onCoverClicked: () -> Unit, // For top action menu onShareClicked: (() -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List) -> Unit, // For swipe actions onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) { val layoutDirection = LocalLayoutDirection.current val density = LocalDensity.current val (chapters, listItem, isAnySelected) = remember(state) { Triple( first = state.processedChapters, second = state.chapterListItems, third = state.isAnySelected, ) } val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() var topBarHeight by remember { mutableIntStateOf(0) } val chapterListState = rememberLazyListState() val internalOnBackPressed = { if (isAnySelected) { onAllChapterSelected(false) } else { onBackClicked() } } BackHandler(onBack = internalOnBackPressed) Scaffold( topBar = { val selectedChapterCount = remember(chapters) { chapters.count { it.selected } } MangaToolbar( modifier = Modifier.onSizeChanged { topBarHeight = it.height }, title = state.manga.title, titleAlphaProvider = { if (isAnySelected) 1f else 0f }, backgroundAlphaProvider = { 1f }, hasFilters = state.filterActive, onBackClicked = internalOnBackPressed, onClickFilter = onFilterButtonClicked, onClickShare = onShareClicked, onClickDownload = onDownloadActionClicked, onClickEditCategory = onEditCategoryClicked, onClickRefresh = onRefresh, onClickMigrate = onMigrateClicked, actionModeCounter = selectedChapterCount, onSelectAll = { onAllChapterSelected(true) }, onInvertSelection = { onInvertSelection() }, ) }, bottomBar = { Box( modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.BottomEnd, ) { val selectedChapters = remember(chapters) { chapters.filter { it.selected } } SharedMangaBottomActionMenu( selected = selectedChapters, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, onDownloadChapter = onDownloadChapter, onMultiDeleteClicked = onMultiDeleteClicked, fillFraction = 0.5f, ) } }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, floatingActionButton = { val isFABVisible = remember(chapters) { chapters.fastAny { !it.chapter.read } && !isAnySelected } AnimatedVisibility( visible = isFABVisible, enter = fadeIn(), exit = fadeOut(), ) { ExtendedFloatingActionButton( text = { val isReading = remember(state.chapters) { state.chapters.fastAny { it.chapter.read } } Text( text = stringResource( if (isReading) MR.strings.action_resume else MR.strings.action_start, ), ) }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, onClick = onContinueReading, expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), ) } }, ) { contentPadding -> PullRefresh( refreshing = state.isRefreshingData, onRefresh = onRefresh, enabled = { !isAnySelected }, indicatorPadding = PaddingValues( start = insetPadding.calculateStartPadding(layoutDirection), top = with(density) { topBarHeight.toDp() }, end = insetPadding.calculateEndPadding(layoutDirection), ), ) { TwoPanelBox( modifier = Modifier.padding( start = contentPadding.calculateStartPadding(layoutDirection), end = contentPadding.calculateEndPadding(layoutDirection), ), startContent = { Column( modifier = Modifier .verticalScroll(rememberScrollState()) .padding(bottom = contentPadding.calculateBottomPadding()), ) { MangaInfoBox( isTabletUi = true, 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 StubSource }, coverDataProvider = { state.manga }, status = state.manga.status, onCoverClick = onCoverClicked, doSearch = onSearch, ) MangaActionRow( favorite = state.manga.favorite, trackingCount = state.trackingCount, nextUpdate = nextUpdate, isUserIntervalMode = state.manga.fetchInterval < 0, onAddToLibraryClicked = onAddToLibraryClicked, onWebViewClicked = onWebViewClicked, onWebViewLongClicked = onWebViewLongClicked, onTrackingClicked = onTrackingClicked, onEditIntervalClicked = onEditIntervalClicked, onEditCategory = onEditCategoryClicked, ) ExpandableMangaDescription( defaultExpandState = true, description = state.manga.description, tagsProvider = { state.manga.genre }, onTagSearch = onTagSearch, onCopyTagToClipboard = onCopyTagToClipboard, ) } }, endContent = { VerticalFastScroller( listState = chapterListState, topContentPadding = contentPadding.calculateTopPadding(), ) { LazyColumn( modifier = Modifier.fillMaxHeight(), state = chapterListState, contentPadding = PaddingValues( top = contentPadding.calculateTopPadding(), bottom = contentPadding.calculateBottomPadding(), ), ) { item( key = MangaScreenItem.CHAPTER_HEADER, contentType = MangaScreenItem.CHAPTER_HEADER, ) { val missingChapterCount = remember(chapters) { chapters.map { it.chapter.chapterNumber }.missingChaptersCount() } ChapterHeader( enabled = !isAnySelected, chapterCount = chapters.size, missingChapterCount = missingChapterCount, onClick = onFilterButtonClicked, ) } sharedChapterItems( manga = state.manga, chapters = listItem, isAnyChapterSelected = chapters.fastAny { it.selected }, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, onChapterClicked = onChapterClicked, onDownloadChapter = onDownloadChapter, onChapterSelected = onChapterSelected, onChapterSwipe = onChapterSwipe, ) } } }, ) } } } @Composable private fun SharedMangaBottomActionMenu( selected: List, onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onMultiDeleteClicked: (List) -> Unit, fillFraction: Float, modifier: Modifier = Modifier, ) { MangaBottomActionMenu( visible = selected.isNotEmpty(), modifier = modifier.fillMaxWidth(fillFraction), onBookmarkClicked = { onMultiBookmarkClicked.invoke(selected.fastMap { it.chapter }, true) }.takeIf { selected.fastAny { !it.chapter.bookmark } }, onRemoveBookmarkClicked = { onMultiBookmarkClicked.invoke(selected.fastMap { it.chapter }, false) }.takeIf { selected.fastAll { it.chapter.bookmark } }, onMarkAsReadClicked = { onMultiMarkAsReadClicked(selected.fastMap { it.chapter }, true) }.takeIf { selected.fastAny { !it.chapter.read } }, onMarkAsUnreadClicked = { onMultiMarkAsReadClicked(selected.fastMap { it.chapter }, false) }.takeIf { selected.fastAny { it.chapter.read || it.chapter.lastPageRead > 0L } }, onMarkPreviousAsReadClicked = { onMarkPreviousAsReadClicked(selected[0].chapter) }.takeIf { selected.size == 1 }, onDownloadClicked = { onDownloadChapter!!(selected.toList(), ChapterDownloadAction.START) }.takeIf { onDownloadChapter != null && selected.fastAny { it.downloadState != Download.State.DOWNLOADED } }, onDeleteClicked = { onMultiDeleteClicked(selected.fastMap { it.chapter }) }.takeIf { selected.fastAny { it.downloadState == Download.State.DOWNLOADED } }, ) } private fun LazyListScope.sharedChapterItems( manga: Manga, chapters: List, isAnyChapterSelected: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, ) { items( items = chapters, key = { item -> when (item) { is ChapterList.MissingCount -> "missing-count-${item.id}" is ChapterList.Item -> "chapter-${item.id}" } }, contentType = { MangaScreenItem.CHAPTER }, ) { item -> val haptic = LocalHapticFeedback.current when (item) { is ChapterList.MissingCount -> { MissingChapterCountListItem(count = item.count) } is ChapterList.Item -> { MangaChapterListItem( title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) { stringResource( MR.strings.display_mode_chapter, formatChapterNumber(item.chapter.chapterNumber), ) } else { item.chapter.name }, date = relativeDateText(item.chapter.dateUpload), readProgress = item.chapter.lastPageRead .takeIf { !item.chapter.read && it > 0L } ?.let { stringResource( MR.strings.chapter_progress, it + 1, ) }, scanlator = item.chapter.scanlator.takeIf { !it.isNullOrBlank() }, read = item.chapter.read, bookmark = item.chapter.bookmark, selected = item.selected, downloadIndicatorEnabled = !isAnyChapterSelected && !manga.isLocal(), downloadStateProvider = { item.downloadState }, downloadProgressProvider = { item.downloadProgress }, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, onLongClick = { onChapterSelected(item, !item.selected, true, true) haptic.performHapticFeedback(HapticFeedbackType.LongPress) }, onClick = { onChapterItemClick( chapterItem = item, isAnyChapterSelected = isAnyChapterSelected, onToggleSelection = { onChapterSelected(item, !item.selected, true, false) }, onChapterClicked = onChapterClicked, ) }, onDownloadClick = if (onDownloadChapter != null) { { onDownloadChapter(listOf(item), it) } } else { null }, onChapterSwipe = { onChapterSwipe(item, it) }, ) } } } } private fun onChapterItemClick( chapterItem: ChapterList.Item, isAnyChapterSelected: Boolean, onToggleSelection: (Boolean) -> Unit, onChapterClicked: (Chapter) -> Unit, ) { when { chapterItem.selected -> onToggleSelection(false) isAnyChapterSelected -> onToggleSelection(true) else -> onChapterClicked(chapterItem.chapter) } }