MangaScreen.kt 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. package eu.kanade.presentation.manga
  2. import androidx.activity.compose.BackHandler
  3. import androidx.compose.animation.AnimatedVisibility
  4. import androidx.compose.animation.core.animateFloatAsState
  5. import androidx.compose.animation.fadeIn
  6. import androidx.compose.animation.fadeOut
  7. import androidx.compose.foundation.layout.Box
  8. import androidx.compose.foundation.layout.Column
  9. import androidx.compose.foundation.layout.PaddingValues
  10. import androidx.compose.foundation.layout.WindowInsets
  11. import androidx.compose.foundation.layout.WindowInsetsSides
  12. import androidx.compose.foundation.layout.asPaddingValues
  13. import androidx.compose.foundation.layout.calculateEndPadding
  14. import androidx.compose.foundation.layout.calculateStartPadding
  15. import androidx.compose.foundation.layout.fillMaxHeight
  16. import androidx.compose.foundation.layout.fillMaxWidth
  17. import androidx.compose.foundation.layout.navigationBars
  18. import androidx.compose.foundation.layout.only
  19. import androidx.compose.foundation.layout.padding
  20. import androidx.compose.foundation.layout.systemBars
  21. import androidx.compose.foundation.lazy.LazyListScope
  22. import androidx.compose.foundation.lazy.items
  23. import androidx.compose.foundation.lazy.rememberLazyListState
  24. import androidx.compose.foundation.rememberScrollState
  25. import androidx.compose.foundation.verticalScroll
  26. import androidx.compose.material.icons.Icons
  27. import androidx.compose.material.icons.filled.PlayArrow
  28. import androidx.compose.material3.Icon
  29. import androidx.compose.material3.SnackbarHost
  30. import androidx.compose.material3.SnackbarHostState
  31. import androidx.compose.material3.Text
  32. import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
  33. import androidx.compose.runtime.Composable
  34. import androidx.compose.runtime.derivedStateOf
  35. import androidx.compose.runtime.getValue
  36. import androidx.compose.runtime.mutableStateOf
  37. import androidx.compose.runtime.remember
  38. import androidx.compose.ui.Alignment
  39. import androidx.compose.ui.Modifier
  40. import androidx.compose.ui.hapticfeedback.HapticFeedbackType
  41. import androidx.compose.ui.layout.onSizeChanged
  42. import androidx.compose.ui.platform.LocalDensity
  43. import androidx.compose.ui.platform.LocalHapticFeedback
  44. import androidx.compose.ui.platform.LocalLayoutDirection
  45. import androidx.compose.ui.res.stringResource
  46. import eu.kanade.domain.chapter.model.Chapter
  47. import eu.kanade.presentation.components.ChapterDownloadAction
  48. import eu.kanade.presentation.components.ExtendedFloatingActionButton
  49. import eu.kanade.presentation.components.LazyColumn
  50. import eu.kanade.presentation.components.MangaBottomActionMenu
  51. import eu.kanade.presentation.components.Scaffold
  52. import eu.kanade.presentation.components.SwipeRefresh
  53. import eu.kanade.presentation.components.TwoPanelBox
  54. import eu.kanade.presentation.components.VerticalFastScroller
  55. import eu.kanade.presentation.manga.components.ChapterHeader
  56. import eu.kanade.presentation.manga.components.ExpandableMangaDescription
  57. import eu.kanade.presentation.manga.components.MangaActionRow
  58. import eu.kanade.presentation.manga.components.MangaChapterListItem
  59. import eu.kanade.presentation.manga.components.MangaInfoBox
  60. import eu.kanade.presentation.manga.components.MangaToolbar
  61. import eu.kanade.presentation.util.isScrolledToEnd
  62. import eu.kanade.presentation.util.isScrollingUp
  63. import eu.kanade.tachiyomi.R
  64. import eu.kanade.tachiyomi.data.download.model.Download
  65. import eu.kanade.tachiyomi.source.SourceManager
  66. import eu.kanade.tachiyomi.source.getNameForMangaInfo
  67. import eu.kanade.tachiyomi.ui.manga.ChapterItem
  68. import eu.kanade.tachiyomi.ui.manga.MangaScreenState
  69. @Composable
  70. fun MangaScreen(
  71. state: MangaScreenState.Success,
  72. snackbarHostState: SnackbarHostState,
  73. windowWidthSizeClass: WindowWidthSizeClass,
  74. onBackClicked: () -> Unit,
  75. onChapterClicked: (Chapter) -> Unit,
  76. onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
  77. onAddToLibraryClicked: () -> Unit,
  78. onWebViewClicked: (() -> Unit)?,
  79. onTrackingClicked: (() -> Unit)?,
  80. onTagClicked: (String) -> Unit,
  81. onFilterButtonClicked: () -> Unit,
  82. onRefresh: () -> Unit,
  83. onContinueReading: () -> Unit,
  84. onSearch: (query: String, global: Boolean) -> Unit,
  85. // For cover dialog
  86. onCoverClicked: () -> Unit,
  87. // For top action menu
  88. onShareClicked: (() -> Unit)?,
  89. onDownloadActionClicked: ((DownloadAction) -> Unit)?,
  90. onEditCategoryClicked: (() -> Unit)?,
  91. onMigrateClicked: (() -> Unit)?,
  92. // For bottom action menu
  93. onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
  94. onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
  95. onMarkPreviousAsReadClicked: (Chapter) -> Unit,
  96. onMultiDeleteClicked: (List<Chapter>) -> Unit,
  97. // Chapter selection
  98. onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
  99. onAllChapterSelected: (Boolean) -> Unit,
  100. onInvertSelection: () -> Unit,
  101. ) {
  102. if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
  103. MangaScreenSmallImpl(
  104. state = state,
  105. snackbarHostState = snackbarHostState,
  106. onBackClicked = onBackClicked,
  107. onChapterClicked = onChapterClicked,
  108. onDownloadChapter = onDownloadChapter,
  109. onAddToLibraryClicked = onAddToLibraryClicked,
  110. onWebViewClicked = onWebViewClicked,
  111. onTrackingClicked = onTrackingClicked,
  112. onTagClicked = onTagClicked,
  113. onFilterClicked = onFilterButtonClicked,
  114. onRefresh = onRefresh,
  115. onContinueReading = onContinueReading,
  116. onSearch = onSearch,
  117. onCoverClicked = onCoverClicked,
  118. onShareClicked = onShareClicked,
  119. onDownloadActionClicked = onDownloadActionClicked,
  120. onEditCategoryClicked = onEditCategoryClicked,
  121. onMigrateClicked = onMigrateClicked,
  122. onMultiBookmarkClicked = onMultiBookmarkClicked,
  123. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  124. onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
  125. onMultiDeleteClicked = onMultiDeleteClicked,
  126. onChapterSelected = onChapterSelected,
  127. onAllChapterSelected = onAllChapterSelected,
  128. onInvertSelection = onInvertSelection,
  129. )
  130. } else {
  131. MangaScreenLargeImpl(
  132. state = state,
  133. windowWidthSizeClass = windowWidthSizeClass,
  134. snackbarHostState = snackbarHostState,
  135. onBackClicked = onBackClicked,
  136. onChapterClicked = onChapterClicked,
  137. onDownloadChapter = onDownloadChapter,
  138. onAddToLibraryClicked = onAddToLibraryClicked,
  139. onWebViewClicked = onWebViewClicked,
  140. onTrackingClicked = onTrackingClicked,
  141. onTagClicked = onTagClicked,
  142. onFilterButtonClicked = onFilterButtonClicked,
  143. onRefresh = onRefresh,
  144. onContinueReading = onContinueReading,
  145. onSearch = onSearch,
  146. onCoverClicked = onCoverClicked,
  147. onShareClicked = onShareClicked,
  148. onDownloadActionClicked = onDownloadActionClicked,
  149. onEditCategoryClicked = onEditCategoryClicked,
  150. onMigrateClicked = onMigrateClicked,
  151. onMultiBookmarkClicked = onMultiBookmarkClicked,
  152. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  153. onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
  154. onMultiDeleteClicked = onMultiDeleteClicked,
  155. onChapterSelected = onChapterSelected,
  156. onAllChapterSelected = onAllChapterSelected,
  157. onInvertSelection = onInvertSelection,
  158. )
  159. }
  160. }
  161. @Composable
  162. private fun MangaScreenSmallImpl(
  163. state: MangaScreenState.Success,
  164. snackbarHostState: SnackbarHostState,
  165. onBackClicked: () -> Unit,
  166. onChapterClicked: (Chapter) -> Unit,
  167. onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
  168. onAddToLibraryClicked: () -> Unit,
  169. onWebViewClicked: (() -> Unit)?,
  170. onTrackingClicked: (() -> Unit)?,
  171. onTagClicked: (String) -> Unit,
  172. onFilterClicked: () -> Unit,
  173. onRefresh: () -> Unit,
  174. onContinueReading: () -> Unit,
  175. onSearch: (query: String, global: Boolean) -> Unit,
  176. // For cover dialog
  177. onCoverClicked: () -> Unit,
  178. // For top action menu
  179. onShareClicked: (() -> Unit)?,
  180. onDownloadActionClicked: ((DownloadAction) -> Unit)?,
  181. onEditCategoryClicked: (() -> Unit)?,
  182. onMigrateClicked: (() -> Unit)?,
  183. // For bottom action menu
  184. onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
  185. onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
  186. onMarkPreviousAsReadClicked: (Chapter) -> Unit,
  187. onMultiDeleteClicked: (List<Chapter>) -> Unit,
  188. // Chapter selection
  189. onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
  190. onAllChapterSelected: (Boolean) -> Unit,
  191. onInvertSelection: () -> Unit,
  192. ) {
  193. val chapterListState = rememberLazyListState()
  194. val chapters = remember(state) { state.processedChapters.toList() }
  195. val internalOnBackPressed = {
  196. if (chapters.any { it.selected }) {
  197. onAllChapterSelected(false)
  198. } else {
  199. onBackClicked()
  200. }
  201. }
  202. BackHandler(onBack = internalOnBackPressed)
  203. Scaffold(
  204. modifier = Modifier
  205. .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal).asPaddingValues()),
  206. topBar = {
  207. val firstVisibleItemIndex by remember {
  208. derivedStateOf { chapterListState.firstVisibleItemIndex }
  209. }
  210. val firstVisibleItemScrollOffset by remember {
  211. derivedStateOf { chapterListState.firstVisibleItemScrollOffset }
  212. }
  213. val animatedTitleAlpha by animateFloatAsState(
  214. if (firstVisibleItemIndex > 0) 1f else 0f,
  215. )
  216. val animatedBgAlpha by animateFloatAsState(
  217. if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
  218. )
  219. MangaToolbar(
  220. title = state.manga.title,
  221. titleAlphaProvider = { animatedTitleAlpha },
  222. backgroundAlphaProvider = { animatedBgAlpha },
  223. hasFilters = state.manga.chaptersFiltered(),
  224. incognitoMode = state.isIncognitoMode,
  225. downloadedOnlyMode = state.isDownloadedOnlyMode,
  226. onBackClicked = internalOnBackPressed,
  227. onClickFilter = onFilterClicked,
  228. onClickShare = onShareClicked,
  229. onClickDownload = onDownloadActionClicked,
  230. onClickEditCategory = onEditCategoryClicked,
  231. onClickMigrate = onMigrateClicked,
  232. actionModeCounter = chapters.count { it.selected },
  233. onSelectAll = { onAllChapterSelected(true) },
  234. onInvertSelection = { onInvertSelection() },
  235. )
  236. },
  237. bottomBar = {
  238. SharedMangaBottomActionMenu(
  239. selected = chapters.filter { it.selected },
  240. onMultiBookmarkClicked = onMultiBookmarkClicked,
  241. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  242. onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
  243. onDownloadChapter = onDownloadChapter,
  244. onMultiDeleteClicked = onMultiDeleteClicked,
  245. fillFraction = 1f,
  246. )
  247. },
  248. snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
  249. floatingActionButton = {
  250. AnimatedVisibility(
  251. visible = chapters.any { !it.chapter.read } && chapters.none { it.selected },
  252. enter = fadeIn(),
  253. exit = fadeOut(),
  254. ) {
  255. ExtendedFloatingActionButton(
  256. text = {
  257. val id = if (chapters.any { it.chapter.read }) {
  258. R.string.action_resume
  259. } else {
  260. R.string.action_start
  261. }
  262. Text(text = stringResource(id))
  263. },
  264. icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
  265. onClick = onContinueReading,
  266. expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
  267. modifier = Modifier
  268. .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
  269. )
  270. }
  271. },
  272. ) { contentPadding ->
  273. val topPadding = contentPadding.calculateTopPadding()
  274. SwipeRefresh(
  275. refreshing = state.isRefreshingData,
  276. onRefresh = onRefresh,
  277. enabled = chapters.none { it.selected },
  278. indicatorPadding = contentPadding,
  279. ) {
  280. VerticalFastScroller(
  281. listState = chapterListState,
  282. topContentPadding = topPadding,
  283. ) {
  284. LazyColumn(
  285. modifier = Modifier.fillMaxHeight(),
  286. state = chapterListState,
  287. contentPadding = PaddingValues(
  288. bottom = contentPadding.calculateBottomPadding(),
  289. ),
  290. ) {
  291. item(
  292. key = MangaScreenItem.INFO_BOX,
  293. contentType = MangaScreenItem.INFO_BOX,
  294. ) {
  295. MangaInfoBox(
  296. windowWidthSizeClass = WindowWidthSizeClass.Compact,
  297. appBarPadding = topPadding,
  298. title = state.manga.title,
  299. author = state.manga.author,
  300. artist = state.manga.artist,
  301. sourceName = remember { state.source.getNameForMangaInfo() },
  302. isStubSource = remember { state.source is SourceManager.StubSource },
  303. coverDataProvider = { state.manga },
  304. status = state.manga.status,
  305. onCoverClick = onCoverClicked,
  306. doSearch = onSearch,
  307. )
  308. }
  309. item(
  310. key = MangaScreenItem.ACTION_ROW,
  311. contentType = MangaScreenItem.ACTION_ROW,
  312. ) {
  313. MangaActionRow(
  314. favorite = state.manga.favorite,
  315. trackingCount = state.trackingCount,
  316. onAddToLibraryClicked = onAddToLibraryClicked,
  317. onWebViewClicked = onWebViewClicked,
  318. onTrackingClicked = onTrackingClicked,
  319. onEditCategory = onEditCategoryClicked,
  320. )
  321. }
  322. item(
  323. key = MangaScreenItem.DESCRIPTION_WITH_TAG,
  324. contentType = MangaScreenItem.DESCRIPTION_WITH_TAG,
  325. ) {
  326. ExpandableMangaDescription(
  327. defaultExpandState = state.isFromSource,
  328. description = state.manga.description,
  329. tagsProvider = { state.manga.genre },
  330. onTagClicked = onTagClicked,
  331. )
  332. }
  333. item(
  334. key = MangaScreenItem.CHAPTER_HEADER,
  335. contentType = MangaScreenItem.CHAPTER_HEADER,
  336. ) {
  337. ChapterHeader(
  338. chapterCount = chapters.size,
  339. onClick = onFilterClicked,
  340. )
  341. }
  342. sharedChapterItems(
  343. chapters = chapters,
  344. onChapterClicked = onChapterClicked,
  345. onDownloadChapter = onDownloadChapter,
  346. onChapterSelected = onChapterSelected,
  347. )
  348. }
  349. }
  350. }
  351. }
  352. }
  353. @Composable
  354. fun MangaScreenLargeImpl(
  355. state: MangaScreenState.Success,
  356. windowWidthSizeClass: WindowWidthSizeClass,
  357. snackbarHostState: SnackbarHostState,
  358. onBackClicked: () -> Unit,
  359. onChapterClicked: (Chapter) -> Unit,
  360. onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
  361. onAddToLibraryClicked: () -> Unit,
  362. onWebViewClicked: (() -> Unit)?,
  363. onTrackingClicked: (() -> Unit)?,
  364. onTagClicked: (String) -> Unit,
  365. onFilterButtonClicked: () -> Unit,
  366. onRefresh: () -> Unit,
  367. onContinueReading: () -> Unit,
  368. onSearch: (query: String, global: Boolean) -> Unit,
  369. // For cover dialog
  370. onCoverClicked: () -> Unit,
  371. // For top action menu
  372. onShareClicked: (() -> Unit)?,
  373. onDownloadActionClicked: ((DownloadAction) -> Unit)?,
  374. onEditCategoryClicked: (() -> Unit)?,
  375. onMigrateClicked: (() -> Unit)?,
  376. // For bottom action menu
  377. onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
  378. onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
  379. onMarkPreviousAsReadClicked: (Chapter) -> Unit,
  380. onMultiDeleteClicked: (List<Chapter>) -> Unit,
  381. // Chapter selection
  382. onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
  383. onAllChapterSelected: (Boolean) -> Unit,
  384. onInvertSelection: () -> Unit,
  385. ) {
  386. val layoutDirection = LocalLayoutDirection.current
  387. val density = LocalDensity.current
  388. val chapters = remember(state) { state.processedChapters.toList() }
  389. val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
  390. val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) }
  391. SwipeRefresh(
  392. refreshing = state.isRefreshingData,
  393. onRefresh = onRefresh,
  394. enabled = chapters.none { it.selected },
  395. indicatorPadding = PaddingValues(
  396. start = insetPadding.calculateStartPadding(layoutDirection),
  397. top = with(density) { topBarHeight.toDp() },
  398. end = insetPadding.calculateEndPadding(layoutDirection),
  399. ),
  400. ) {
  401. val chapterListState = rememberLazyListState()
  402. val internalOnBackPressed = {
  403. if (chapters.any { it.selected }) {
  404. onAllChapterSelected(false)
  405. } else {
  406. onBackClicked()
  407. }
  408. }
  409. BackHandler(onBack = internalOnBackPressed)
  410. Scaffold(
  411. modifier = Modifier.padding(insetPadding),
  412. topBar = {
  413. MangaToolbar(
  414. modifier = Modifier.onSizeChanged { onTopBarHeightChanged(it.height) },
  415. title = state.manga.title,
  416. titleAlphaProvider = { if (chapters.any { it.selected }) 1f else 0f },
  417. backgroundAlphaProvider = { 1f },
  418. hasFilters = state.manga.chaptersFiltered(),
  419. incognitoMode = state.isIncognitoMode,
  420. downloadedOnlyMode = state.isDownloadedOnlyMode,
  421. onBackClicked = internalOnBackPressed,
  422. onClickFilter = onFilterButtonClicked,
  423. onClickShare = onShareClicked,
  424. onClickDownload = onDownloadActionClicked,
  425. onClickEditCategory = onEditCategoryClicked,
  426. onClickMigrate = onMigrateClicked,
  427. actionModeCounter = chapters.count { it.selected },
  428. onSelectAll = { onAllChapterSelected(true) },
  429. onInvertSelection = { onInvertSelection() },
  430. )
  431. },
  432. bottomBar = {
  433. Box(
  434. modifier = Modifier.fillMaxWidth(),
  435. contentAlignment = Alignment.BottomEnd,
  436. ) {
  437. SharedMangaBottomActionMenu(
  438. selected = chapters.filter { it.selected },
  439. onMultiBookmarkClicked = onMultiBookmarkClicked,
  440. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  441. onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
  442. onDownloadChapter = onDownloadChapter,
  443. onMultiDeleteClicked = onMultiDeleteClicked,
  444. fillFraction = 0.5f,
  445. )
  446. }
  447. },
  448. snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
  449. floatingActionButton = {
  450. AnimatedVisibility(
  451. visible = chapters.any { !it.chapter.read } && chapters.none { it.selected },
  452. enter = fadeIn(),
  453. exit = fadeOut(),
  454. ) {
  455. ExtendedFloatingActionButton(
  456. text = {
  457. val id = if (chapters.any { it.chapter.read }) {
  458. R.string.action_resume
  459. } else {
  460. R.string.action_start
  461. }
  462. Text(text = stringResource(id))
  463. },
  464. icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
  465. onClick = onContinueReading,
  466. expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
  467. modifier = Modifier
  468. .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
  469. )
  470. }
  471. },
  472. ) { contentPadding ->
  473. TwoPanelBox(
  474. startContent = {
  475. Column(
  476. modifier = Modifier
  477. .verticalScroll(rememberScrollState()),
  478. ) {
  479. MangaInfoBox(
  480. windowWidthSizeClass = windowWidthSizeClass,
  481. appBarPadding = contentPadding.calculateTopPadding(),
  482. title = state.manga.title,
  483. author = state.manga.author,
  484. artist = state.manga.artist,
  485. sourceName = remember { state.source.getNameForMangaInfo() },
  486. isStubSource = remember { state.source is SourceManager.StubSource },
  487. coverDataProvider = { state.manga },
  488. status = state.manga.status,
  489. onCoverClick = onCoverClicked,
  490. doSearch = onSearch,
  491. )
  492. MangaActionRow(
  493. favorite = state.manga.favorite,
  494. trackingCount = state.trackingCount,
  495. onAddToLibraryClicked = onAddToLibraryClicked,
  496. onWebViewClicked = onWebViewClicked,
  497. onTrackingClicked = onTrackingClicked,
  498. onEditCategory = onEditCategoryClicked,
  499. )
  500. ExpandableMangaDescription(
  501. defaultExpandState = true,
  502. description = state.manga.description,
  503. tagsProvider = { state.manga.genre },
  504. onTagClicked = onTagClicked,
  505. )
  506. }
  507. },
  508. endContent = {
  509. VerticalFastScroller(
  510. listState = chapterListState,
  511. topContentPadding = contentPadding.calculateTopPadding(),
  512. ) {
  513. LazyColumn(
  514. modifier = Modifier.fillMaxHeight(),
  515. state = chapterListState,
  516. contentPadding = PaddingValues(
  517. top = contentPadding.calculateTopPadding(),
  518. bottom = contentPadding.calculateBottomPadding(),
  519. ),
  520. ) {
  521. item(
  522. key = MangaScreenItem.CHAPTER_HEADER,
  523. contentType = MangaScreenItem.CHAPTER_HEADER,
  524. ) {
  525. ChapterHeader(
  526. chapterCount = chapters.size,
  527. onClick = onFilterButtonClicked,
  528. )
  529. }
  530. sharedChapterItems(
  531. chapters = chapters,
  532. onChapterClicked = onChapterClicked,
  533. onDownloadChapter = onDownloadChapter,
  534. onChapterSelected = onChapterSelected,
  535. )
  536. }
  537. }
  538. },
  539. )
  540. }
  541. }
  542. }
  543. @Composable
  544. private fun SharedMangaBottomActionMenu(
  545. selected: List<ChapterItem>,
  546. modifier: Modifier = Modifier,
  547. onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
  548. onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
  549. onMarkPreviousAsReadClicked: (Chapter) -> Unit,
  550. onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
  551. onMultiDeleteClicked: (List<Chapter>) -> Unit,
  552. fillFraction: Float,
  553. ) {
  554. MangaBottomActionMenu(
  555. visible = selected.isNotEmpty(),
  556. modifier = modifier.fillMaxWidth(fillFraction),
  557. onBookmarkClicked = {
  558. onMultiBookmarkClicked.invoke(selected.map { it.chapter }, true)
  559. }.takeIf { selected.any { !it.chapter.bookmark } },
  560. onRemoveBookmarkClicked = {
  561. onMultiBookmarkClicked.invoke(selected.map { it.chapter }, false)
  562. }.takeIf { selected.all { it.chapter.bookmark } },
  563. onMarkAsReadClicked = {
  564. onMultiMarkAsReadClicked(selected.map { it.chapter }, true)
  565. }.takeIf { selected.any { !it.chapter.read } },
  566. onMarkAsUnreadClicked = {
  567. onMultiMarkAsReadClicked(selected.map { it.chapter }, false)
  568. }.takeIf { selected.any { it.chapter.read || it.chapter.lastPageRead > 0L } },
  569. onMarkPreviousAsReadClicked = {
  570. onMarkPreviousAsReadClicked(selected[0].chapter)
  571. }.takeIf { selected.size == 1 },
  572. onDownloadClicked = {
  573. onDownloadChapter!!(selected.toList(), ChapterDownloadAction.START)
  574. }.takeIf {
  575. onDownloadChapter != null && selected.any { it.downloadState != Download.State.DOWNLOADED }
  576. },
  577. onDeleteClicked = {
  578. onMultiDeleteClicked(selected.map { it.chapter })
  579. }.takeIf {
  580. onDownloadChapter != null && selected.any { it.downloadState == Download.State.DOWNLOADED }
  581. },
  582. )
  583. }
  584. private fun LazyListScope.sharedChapterItems(
  585. chapters: List<ChapterItem>,
  586. onChapterClicked: (Chapter) -> Unit,
  587. onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
  588. onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
  589. ) {
  590. items(
  591. items = chapters,
  592. key = { "chapter-${it.chapter.id}" },
  593. contentType = { MangaScreenItem.CHAPTER },
  594. ) { chapterItem ->
  595. val haptic = LocalHapticFeedback.current
  596. MangaChapterListItem(
  597. title = chapterItem.chapterTitleString,
  598. date = chapterItem.dateUploadString,
  599. readProgress = chapterItem.readProgressString,
  600. scanlator = chapterItem.chapter.scanlator.takeIf { !it.isNullOrBlank() },
  601. read = chapterItem.chapter.read,
  602. bookmark = chapterItem.chapter.bookmark,
  603. selected = chapterItem.selected,
  604. downloadStateProvider = { chapterItem.downloadState },
  605. downloadProgressProvider = { chapterItem.downloadProgress },
  606. onLongClick = {
  607. onChapterSelected(chapterItem, !chapterItem.selected, true, true)
  608. haptic.performHapticFeedback(HapticFeedbackType.LongPress)
  609. },
  610. onClick = {
  611. onChapterItemClick(
  612. chapterItem = chapterItem,
  613. chapters = chapters,
  614. onToggleSelection = { onChapterSelected(chapterItem, !chapterItem.selected, true, false) },
  615. onChapterClicked = onChapterClicked,
  616. )
  617. },
  618. onDownloadClick = if (onDownloadChapter != null) {
  619. { onDownloadChapter(listOf(chapterItem), it) }
  620. } else {
  621. null
  622. },
  623. )
  624. }
  625. }
  626. private fun onChapterItemClick(
  627. chapterItem: ChapterItem,
  628. chapters: List<ChapterItem>,
  629. onToggleSelection: (Boolean) -> Unit,
  630. onChapterClicked: (Chapter) -> Unit,
  631. ) {
  632. when {
  633. chapterItem.selected -> onToggleSelection(false)
  634. chapters.any { it.selected } -> onToggleSelection(true)
  635. else -> onChapterClicked(chapterItem.chapter)
  636. }
  637. }