MangaScreen.kt 29 KB

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