MangaScreen.kt 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838
  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.LazyColumn
  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.runtime.Composable
  33. import androidx.compose.runtime.derivedStateOf
  34. import androidx.compose.runtime.getValue
  35. import androidx.compose.runtime.mutableIntStateOf
  36. import androidx.compose.runtime.remember
  37. import androidx.compose.runtime.setValue
  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.LocalContext
  43. import androidx.compose.ui.platform.LocalDensity
  44. import androidx.compose.ui.platform.LocalHapticFeedback
  45. import androidx.compose.ui.platform.LocalLayoutDirection
  46. import androidx.compose.ui.util.fastAll
  47. import androidx.compose.ui.util.fastAny
  48. import androidx.compose.ui.util.fastMap
  49. import eu.kanade.presentation.components.relativeDateText
  50. import eu.kanade.presentation.manga.components.ChapterDownloadAction
  51. import eu.kanade.presentation.manga.components.ChapterHeader
  52. import eu.kanade.presentation.manga.components.ExpandableMangaDescription
  53. import eu.kanade.presentation.manga.components.MangaActionRow
  54. import eu.kanade.presentation.manga.components.MangaBottomActionMenu
  55. import eu.kanade.presentation.manga.components.MangaChapterListItem
  56. import eu.kanade.presentation.manga.components.MangaInfoBox
  57. import eu.kanade.presentation.manga.components.MangaToolbar
  58. import eu.kanade.presentation.manga.components.MissingChapterCountListItem
  59. import eu.kanade.presentation.util.formatChapterNumber
  60. import eu.kanade.tachiyomi.data.download.model.Download
  61. import eu.kanade.tachiyomi.source.getNameForMangaInfo
  62. import eu.kanade.tachiyomi.ui.manga.ChapterList
  63. import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
  64. import eu.kanade.tachiyomi.util.system.copyToClipboard
  65. import tachiyomi.domain.chapter.model.Chapter
  66. import tachiyomi.domain.chapter.service.missingChaptersCount
  67. import tachiyomi.domain.library.service.LibraryPreferences
  68. import tachiyomi.domain.manga.model.Manga
  69. import tachiyomi.domain.source.model.StubSource
  70. import tachiyomi.i18n.MR
  71. import tachiyomi.presentation.core.components.TwoPanelBox
  72. import tachiyomi.presentation.core.components.VerticalFastScroller
  73. import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
  74. import tachiyomi.presentation.core.components.material.PullRefresh
  75. import tachiyomi.presentation.core.components.material.Scaffold
  76. import tachiyomi.presentation.core.i18n.stringResource
  77. import tachiyomi.presentation.core.util.isScrolledToEnd
  78. import tachiyomi.presentation.core.util.isScrollingUp
  79. import tachiyomi.source.local.isLocal
  80. import java.time.Instant
  81. @Composable
  82. fun MangaScreen(
  83. state: MangaScreenModel.State.Success,
  84. snackbarHostState: SnackbarHostState,
  85. nextUpdate: Instant?,
  86. isTabletUi: Boolean,
  87. chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
  88. chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
  89. onBackClicked: () -> Unit,
  90. onChapterClicked: (Chapter) -> Unit,
  91. onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
  92. onAddToLibraryClicked: () -> Unit,
  93. onWebViewClicked: (() -> Unit)?,
  94. onWebViewLongClicked: (() -> Unit)?,
  95. onTrackingClicked: () -> Unit,
  96. // For tags menu
  97. onTagSearch: (String) -> Unit,
  98. onFilterButtonClicked: () -> Unit,
  99. onRefresh: () -> Unit,
  100. onContinueReading: () -> Unit,
  101. onSearch: (query: String, global: Boolean) -> Unit,
  102. // For cover dialog
  103. onCoverClicked: () -> Unit,
  104. // For top action menu
  105. onShareClicked: (() -> Unit)?,
  106. onDownloadActionClicked: ((DownloadAction) -> Unit)?,
  107. onEditCategoryClicked: (() -> Unit)?,
  108. onEditFetchIntervalClicked: (() -> Unit)?,
  109. onMigrateClicked: (() -> Unit)?,
  110. // For bottom action menu
  111. onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
  112. onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
  113. onMarkPreviousAsReadClicked: (Chapter) -> Unit,
  114. onMultiDeleteClicked: (List<Chapter>) -> Unit,
  115. // For chapter swipe
  116. onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
  117. // Chapter selection
  118. onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
  119. onAllChapterSelected: (Boolean) -> Unit,
  120. onInvertSelection: () -> Unit,
  121. ) {
  122. val context = LocalContext.current
  123. val onCopyTagToClipboard: (tag: String) -> Unit = {
  124. if (it.isNotEmpty()) {
  125. context.copyToClipboard(it, it)
  126. }
  127. }
  128. if (!isTabletUi) {
  129. MangaScreenSmallImpl(
  130. state = state,
  131. snackbarHostState = snackbarHostState,
  132. nextUpdate = nextUpdate,
  133. chapterSwipeStartAction = chapterSwipeStartAction,
  134. chapterSwipeEndAction = chapterSwipeEndAction,
  135. onBackClicked = onBackClicked,
  136. onChapterClicked = onChapterClicked,
  137. onDownloadChapter = onDownloadChapter,
  138. onAddToLibraryClicked = onAddToLibraryClicked,
  139. onWebViewClicked = onWebViewClicked,
  140. onWebViewLongClicked = onWebViewLongClicked,
  141. onTrackingClicked = onTrackingClicked,
  142. onTagSearch = onTagSearch,
  143. onCopyTagToClipboard = onCopyTagToClipboard,
  144. onFilterClicked = onFilterButtonClicked,
  145. onRefresh = onRefresh,
  146. onContinueReading = onContinueReading,
  147. onSearch = onSearch,
  148. onCoverClicked = onCoverClicked,
  149. onShareClicked = onShareClicked,
  150. onDownloadActionClicked = onDownloadActionClicked,
  151. onEditCategoryClicked = onEditCategoryClicked,
  152. onEditIntervalClicked = onEditFetchIntervalClicked,
  153. onMigrateClicked = onMigrateClicked,
  154. onMultiBookmarkClicked = onMultiBookmarkClicked,
  155. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  156. onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
  157. onMultiDeleteClicked = onMultiDeleteClicked,
  158. onChapterSwipe = onChapterSwipe,
  159. onChapterSelected = onChapterSelected,
  160. onAllChapterSelected = onAllChapterSelected,
  161. onInvertSelection = onInvertSelection,
  162. )
  163. } else {
  164. MangaScreenLargeImpl(
  165. state = state,
  166. snackbarHostState = snackbarHostState,
  167. chapterSwipeStartAction = chapterSwipeStartAction,
  168. chapterSwipeEndAction = chapterSwipeEndAction,
  169. nextUpdate = nextUpdate,
  170. onBackClicked = onBackClicked,
  171. onChapterClicked = onChapterClicked,
  172. onDownloadChapter = onDownloadChapter,
  173. onAddToLibraryClicked = onAddToLibraryClicked,
  174. onWebViewClicked = onWebViewClicked,
  175. onWebViewLongClicked = onWebViewLongClicked,
  176. onTrackingClicked = onTrackingClicked,
  177. onTagSearch = onTagSearch,
  178. onCopyTagToClipboard = onCopyTagToClipboard,
  179. onFilterButtonClicked = onFilterButtonClicked,
  180. onRefresh = onRefresh,
  181. onContinueReading = onContinueReading,
  182. onSearch = onSearch,
  183. onCoverClicked = onCoverClicked,
  184. onShareClicked = onShareClicked,
  185. onDownloadActionClicked = onDownloadActionClicked,
  186. onEditCategoryClicked = onEditCategoryClicked,
  187. onEditIntervalClicked = onEditFetchIntervalClicked,
  188. onMigrateClicked = onMigrateClicked,
  189. onMultiBookmarkClicked = onMultiBookmarkClicked,
  190. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  191. onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
  192. onMultiDeleteClicked = onMultiDeleteClicked,
  193. onChapterSwipe = onChapterSwipe,
  194. onChapterSelected = onChapterSelected,
  195. onAllChapterSelected = onAllChapterSelected,
  196. onInvertSelection = onInvertSelection,
  197. )
  198. }
  199. }
  200. @Composable
  201. private fun MangaScreenSmallImpl(
  202. state: MangaScreenModel.State.Success,
  203. snackbarHostState: SnackbarHostState,
  204. nextUpdate: Instant?,
  205. chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
  206. chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
  207. onBackClicked: () -> Unit,
  208. onChapterClicked: (Chapter) -> Unit,
  209. onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
  210. onAddToLibraryClicked: () -> Unit,
  211. onWebViewClicked: (() -> Unit)?,
  212. onWebViewLongClicked: (() -> Unit)?,
  213. onTrackingClicked: () -> Unit,
  214. // For tags menu
  215. onTagSearch: (String) -> Unit,
  216. onCopyTagToClipboard: (tag: String) -> Unit,
  217. onFilterClicked: () -> Unit,
  218. onRefresh: () -> Unit,
  219. onContinueReading: () -> Unit,
  220. onSearch: (query: String, global: Boolean) -> Unit,
  221. // For cover dialog
  222. onCoverClicked: () -> Unit,
  223. // For top action menu
  224. onShareClicked: (() -> Unit)?,
  225. onDownloadActionClicked: ((DownloadAction) -> Unit)?,
  226. onEditCategoryClicked: (() -> Unit)?,
  227. onEditIntervalClicked: (() -> Unit)?,
  228. onMigrateClicked: (() -> Unit)?,
  229. // For bottom action menu
  230. onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
  231. onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
  232. onMarkPreviousAsReadClicked: (Chapter) -> Unit,
  233. onMultiDeleteClicked: (List<Chapter>) -> Unit,
  234. // For chapter swipe
  235. onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
  236. // Chapter selection
  237. onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
  238. onAllChapterSelected: (Boolean) -> Unit,
  239. onInvertSelection: () -> Unit,
  240. ) {
  241. val chapterListState = rememberLazyListState()
  242. val (chapters, listItem, isAnySelected) = remember(state) {
  243. Triple(
  244. first = state.processedChapters,
  245. second = state.chapterListItems,
  246. third = state.isAnySelected,
  247. )
  248. }
  249. val internalOnBackPressed = {
  250. if (isAnySelected) {
  251. onAllChapterSelected(false)
  252. } else {
  253. onBackClicked()
  254. }
  255. }
  256. BackHandler(onBack = internalOnBackPressed)
  257. Scaffold(
  258. topBar = {
  259. val selectedChapterCount: Int = remember(chapters) {
  260. chapters.count { it.selected }
  261. }
  262. val isFirstItemVisible by remember {
  263. derivedStateOf { chapterListState.firstVisibleItemIndex == 0 }
  264. }
  265. val isFirstItemScrolled by remember {
  266. derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 }
  267. }
  268. val animatedTitleAlpha by animateFloatAsState(
  269. if (!isFirstItemVisible) 1f else 0f,
  270. label = "Top Bar Title",
  271. )
  272. val animatedBgAlpha by animateFloatAsState(
  273. if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
  274. label = "Top Bar Background",
  275. )
  276. MangaToolbar(
  277. title = state.manga.title,
  278. titleAlphaProvider = { animatedTitleAlpha },
  279. backgroundAlphaProvider = { animatedBgAlpha },
  280. hasFilters = state.filterActive,
  281. onBackClicked = internalOnBackPressed,
  282. onClickFilter = onFilterClicked,
  283. onClickShare = onShareClicked,
  284. onClickDownload = onDownloadActionClicked,
  285. onClickEditCategory = onEditCategoryClicked,
  286. onClickRefresh = onRefresh,
  287. onClickMigrate = onMigrateClicked,
  288. actionModeCounter = selectedChapterCount,
  289. onSelectAll = { onAllChapterSelected(true) },
  290. onInvertSelection = { onInvertSelection() },
  291. )
  292. },
  293. bottomBar = {
  294. val selectedChapters = remember(chapters) {
  295. chapters.filter { it.selected }
  296. }
  297. SharedMangaBottomActionMenu(
  298. selected = selectedChapters,
  299. onMultiBookmarkClicked = onMultiBookmarkClicked,
  300. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  301. onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
  302. onDownloadChapter = onDownloadChapter,
  303. onMultiDeleteClicked = onMultiDeleteClicked,
  304. fillFraction = 1f,
  305. )
  306. },
  307. snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
  308. floatingActionButton = {
  309. val isFABVisible = remember(chapters) {
  310. chapters.fastAny { !it.chapter.read } && !isAnySelected
  311. }
  312. AnimatedVisibility(
  313. visible = isFABVisible,
  314. enter = fadeIn(),
  315. exit = fadeOut(),
  316. ) {
  317. ExtendedFloatingActionButton(
  318. text = {
  319. val isReading = remember(state.chapters) {
  320. state.chapters.fastAny { it.chapter.read }
  321. }
  322. Text(
  323. text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
  324. )
  325. },
  326. icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
  327. onClick = onContinueReading,
  328. expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
  329. )
  330. }
  331. },
  332. ) { contentPadding ->
  333. val topPadding = contentPadding.calculateTopPadding()
  334. PullRefresh(
  335. refreshing = state.isRefreshingData,
  336. onRefresh = onRefresh,
  337. enabled = { !isAnySelected },
  338. indicatorPadding = PaddingValues(top = topPadding),
  339. ) {
  340. val layoutDirection = LocalLayoutDirection.current
  341. VerticalFastScroller(
  342. listState = chapterListState,
  343. topContentPadding = topPadding,
  344. endContentPadding = contentPadding.calculateEndPadding(layoutDirection),
  345. ) {
  346. LazyColumn(
  347. modifier = Modifier.fillMaxHeight(),
  348. state = chapterListState,
  349. contentPadding = PaddingValues(
  350. start = contentPadding.calculateStartPadding(layoutDirection),
  351. end = contentPadding.calculateEndPadding(layoutDirection),
  352. bottom = contentPadding.calculateBottomPadding(),
  353. ),
  354. ) {
  355. item(
  356. key = MangaScreenItem.INFO_BOX,
  357. contentType = MangaScreenItem.INFO_BOX,
  358. ) {
  359. MangaInfoBox(
  360. isTabletUi = false,
  361. appBarPadding = topPadding,
  362. title = state.manga.title,
  363. author = state.manga.author,
  364. artist = state.manga.artist,
  365. sourceName = remember { state.source.getNameForMangaInfo() },
  366. isStubSource = remember { state.source is StubSource },
  367. coverDataProvider = { state.manga },
  368. status = state.manga.status,
  369. onCoverClick = onCoverClicked,
  370. doSearch = onSearch,
  371. )
  372. }
  373. item(
  374. key = MangaScreenItem.ACTION_ROW,
  375. contentType = MangaScreenItem.ACTION_ROW,
  376. ) {
  377. MangaActionRow(
  378. favorite = state.manga.favorite,
  379. trackingCount = state.trackingCount,
  380. nextUpdate = nextUpdate,
  381. isUserIntervalMode = state.manga.fetchInterval < 0,
  382. onAddToLibraryClicked = onAddToLibraryClicked,
  383. onWebViewClicked = onWebViewClicked,
  384. onWebViewLongClicked = onWebViewLongClicked,
  385. onTrackingClicked = onTrackingClicked,
  386. onEditIntervalClicked = onEditIntervalClicked,
  387. onEditCategory = onEditCategoryClicked,
  388. )
  389. }
  390. item(
  391. key = MangaScreenItem.DESCRIPTION_WITH_TAG,
  392. contentType = MangaScreenItem.DESCRIPTION_WITH_TAG,
  393. ) {
  394. ExpandableMangaDescription(
  395. defaultExpandState = state.isFromSource,
  396. description = state.manga.description,
  397. tagsProvider = { state.manga.genre },
  398. onTagSearch = onTagSearch,
  399. onCopyTagToClipboard = onCopyTagToClipboard,
  400. )
  401. }
  402. item(
  403. key = MangaScreenItem.CHAPTER_HEADER,
  404. contentType = MangaScreenItem.CHAPTER_HEADER,
  405. ) {
  406. val missingChapterCount = remember(chapters) {
  407. chapters.map { it.chapter.chapterNumber }.missingChaptersCount()
  408. }
  409. ChapterHeader(
  410. enabled = !isAnySelected,
  411. chapterCount = chapters.size,
  412. missingChapterCount = missingChapterCount,
  413. onClick = onFilterClicked,
  414. )
  415. }
  416. sharedChapterItems(
  417. manga = state.manga,
  418. chapters = listItem,
  419. isAnyChapterSelected = chapters.fastAny { it.selected },
  420. chapterSwipeStartAction = chapterSwipeStartAction,
  421. chapterSwipeEndAction = chapterSwipeEndAction,
  422. onChapterClicked = onChapterClicked,
  423. onDownloadChapter = onDownloadChapter,
  424. onChapterSelected = onChapterSelected,
  425. onChapterSwipe = onChapterSwipe,
  426. )
  427. }
  428. }
  429. }
  430. }
  431. }
  432. @Composable
  433. fun MangaScreenLargeImpl(
  434. state: MangaScreenModel.State.Success,
  435. snackbarHostState: SnackbarHostState,
  436. nextUpdate: Instant?,
  437. chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
  438. chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
  439. onBackClicked: () -> Unit,
  440. onChapterClicked: (Chapter) -> Unit,
  441. onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
  442. onAddToLibraryClicked: () -> Unit,
  443. onWebViewClicked: (() -> Unit)?,
  444. onWebViewLongClicked: (() -> Unit)?,
  445. onTrackingClicked: () -> Unit,
  446. // For tags menu
  447. onTagSearch: (String) -> Unit,
  448. onCopyTagToClipboard: (tag: String) -> Unit,
  449. onFilterButtonClicked: () -> Unit,
  450. onRefresh: () -> Unit,
  451. onContinueReading: () -> Unit,
  452. onSearch: (query: String, global: Boolean) -> Unit,
  453. // For cover dialog
  454. onCoverClicked: () -> Unit,
  455. // For top action menu
  456. onShareClicked: (() -> Unit)?,
  457. onDownloadActionClicked: ((DownloadAction) -> Unit)?,
  458. onEditCategoryClicked: (() -> Unit)?,
  459. onEditIntervalClicked: (() -> Unit)?,
  460. onMigrateClicked: (() -> Unit)?,
  461. // For bottom action menu
  462. onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
  463. onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
  464. onMarkPreviousAsReadClicked: (Chapter) -> Unit,
  465. onMultiDeleteClicked: (List<Chapter>) -> Unit,
  466. // For swipe actions
  467. onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
  468. // Chapter selection
  469. onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
  470. onAllChapterSelected: (Boolean) -> Unit,
  471. onInvertSelection: () -> Unit,
  472. ) {
  473. val layoutDirection = LocalLayoutDirection.current
  474. val density = LocalDensity.current
  475. val (chapters, listItem, isAnySelected) = remember(state) {
  476. Triple(
  477. first = state.processedChapters,
  478. second = state.chapterListItems,
  479. third = state.isAnySelected,
  480. )
  481. }
  482. val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
  483. var topBarHeight by remember { mutableIntStateOf(0) }
  484. val chapterListState = rememberLazyListState()
  485. val internalOnBackPressed = {
  486. if (isAnySelected) {
  487. onAllChapterSelected(false)
  488. } else {
  489. onBackClicked()
  490. }
  491. }
  492. BackHandler(onBack = internalOnBackPressed)
  493. Scaffold(
  494. topBar = {
  495. val selectedChapterCount = remember(chapters) {
  496. chapters.count { it.selected }
  497. }
  498. MangaToolbar(
  499. modifier = Modifier.onSizeChanged { topBarHeight = it.height },
  500. title = state.manga.title,
  501. titleAlphaProvider = { if (isAnySelected) 1f else 0f },
  502. backgroundAlphaProvider = { 1f },
  503. hasFilters = state.filterActive,
  504. onBackClicked = internalOnBackPressed,
  505. onClickFilter = onFilterButtonClicked,
  506. onClickShare = onShareClicked,
  507. onClickDownload = onDownloadActionClicked,
  508. onClickEditCategory = onEditCategoryClicked,
  509. onClickRefresh = onRefresh,
  510. onClickMigrate = onMigrateClicked,
  511. actionModeCounter = selectedChapterCount,
  512. onSelectAll = { onAllChapterSelected(true) },
  513. onInvertSelection = { onInvertSelection() },
  514. )
  515. },
  516. bottomBar = {
  517. Box(
  518. modifier = Modifier.fillMaxWidth(),
  519. contentAlignment = Alignment.BottomEnd,
  520. ) {
  521. val selectedChapters = remember(chapters) {
  522. chapters.filter { it.selected }
  523. }
  524. SharedMangaBottomActionMenu(
  525. selected = selectedChapters,
  526. onMultiBookmarkClicked = onMultiBookmarkClicked,
  527. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  528. onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
  529. onDownloadChapter = onDownloadChapter,
  530. onMultiDeleteClicked = onMultiDeleteClicked,
  531. fillFraction = 0.5f,
  532. )
  533. }
  534. },
  535. snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
  536. floatingActionButton = {
  537. val isFABVisible = remember(chapters) {
  538. chapters.fastAny { !it.chapter.read } && !isAnySelected
  539. }
  540. AnimatedVisibility(
  541. visible = isFABVisible,
  542. enter = fadeIn(),
  543. exit = fadeOut(),
  544. ) {
  545. ExtendedFloatingActionButton(
  546. text = {
  547. val isReading = remember(state.chapters) {
  548. state.chapters.fastAny { it.chapter.read }
  549. }
  550. Text(
  551. text = stringResource(
  552. if (isReading) MR.strings.action_resume else MR.strings.action_start,
  553. ),
  554. )
  555. },
  556. icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
  557. onClick = onContinueReading,
  558. expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
  559. )
  560. }
  561. },
  562. ) { contentPadding ->
  563. PullRefresh(
  564. refreshing = state.isRefreshingData,
  565. onRefresh = onRefresh,
  566. enabled = { !isAnySelected },
  567. indicatorPadding = PaddingValues(
  568. start = insetPadding.calculateStartPadding(layoutDirection),
  569. top = with(density) { topBarHeight.toDp() },
  570. end = insetPadding.calculateEndPadding(layoutDirection),
  571. ),
  572. ) {
  573. TwoPanelBox(
  574. modifier = Modifier.padding(
  575. start = contentPadding.calculateStartPadding(layoutDirection),
  576. end = contentPadding.calculateEndPadding(layoutDirection),
  577. ),
  578. startContent = {
  579. Column(
  580. modifier = Modifier
  581. .verticalScroll(rememberScrollState())
  582. .padding(bottom = contentPadding.calculateBottomPadding()),
  583. ) {
  584. MangaInfoBox(
  585. isTabletUi = true,
  586. appBarPadding = contentPadding.calculateTopPadding(),
  587. title = state.manga.title,
  588. author = state.manga.author,
  589. artist = state.manga.artist,
  590. sourceName = remember { state.source.getNameForMangaInfo() },
  591. isStubSource = remember { state.source is StubSource },
  592. coverDataProvider = { state.manga },
  593. status = state.manga.status,
  594. onCoverClick = onCoverClicked,
  595. doSearch = onSearch,
  596. )
  597. MangaActionRow(
  598. favorite = state.manga.favorite,
  599. trackingCount = state.trackingCount,
  600. nextUpdate = nextUpdate,
  601. isUserIntervalMode = state.manga.fetchInterval < 0,
  602. onAddToLibraryClicked = onAddToLibraryClicked,
  603. onWebViewClicked = onWebViewClicked,
  604. onWebViewLongClicked = onWebViewLongClicked,
  605. onTrackingClicked = onTrackingClicked,
  606. onEditIntervalClicked = onEditIntervalClicked,
  607. onEditCategory = onEditCategoryClicked,
  608. )
  609. ExpandableMangaDescription(
  610. defaultExpandState = true,
  611. description = state.manga.description,
  612. tagsProvider = { state.manga.genre },
  613. onTagSearch = onTagSearch,
  614. onCopyTagToClipboard = onCopyTagToClipboard,
  615. )
  616. }
  617. },
  618. endContent = {
  619. VerticalFastScroller(
  620. listState = chapterListState,
  621. topContentPadding = contentPadding.calculateTopPadding(),
  622. ) {
  623. LazyColumn(
  624. modifier = Modifier.fillMaxHeight(),
  625. state = chapterListState,
  626. contentPadding = PaddingValues(
  627. top = contentPadding.calculateTopPadding(),
  628. bottom = contentPadding.calculateBottomPadding(),
  629. ),
  630. ) {
  631. item(
  632. key = MangaScreenItem.CHAPTER_HEADER,
  633. contentType = MangaScreenItem.CHAPTER_HEADER,
  634. ) {
  635. val missingChapterCount = remember(chapters) {
  636. chapters.map { it.chapter.chapterNumber }.missingChaptersCount()
  637. }
  638. ChapterHeader(
  639. enabled = !isAnySelected,
  640. chapterCount = chapters.size,
  641. missingChapterCount = missingChapterCount,
  642. onClick = onFilterButtonClicked,
  643. )
  644. }
  645. sharedChapterItems(
  646. manga = state.manga,
  647. chapters = listItem,
  648. isAnyChapterSelected = chapters.fastAny { it.selected },
  649. chapterSwipeStartAction = chapterSwipeStartAction,
  650. chapterSwipeEndAction = chapterSwipeEndAction,
  651. onChapterClicked = onChapterClicked,
  652. onDownloadChapter = onDownloadChapter,
  653. onChapterSelected = onChapterSelected,
  654. onChapterSwipe = onChapterSwipe,
  655. )
  656. }
  657. }
  658. },
  659. )
  660. }
  661. }
  662. }
  663. @Composable
  664. private fun SharedMangaBottomActionMenu(
  665. selected: List<ChapterList.Item>,
  666. onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
  667. onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
  668. onMarkPreviousAsReadClicked: (Chapter) -> Unit,
  669. onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
  670. onMultiDeleteClicked: (List<Chapter>) -> Unit,
  671. fillFraction: Float,
  672. modifier: Modifier = Modifier,
  673. ) {
  674. MangaBottomActionMenu(
  675. visible = selected.isNotEmpty(),
  676. modifier = modifier.fillMaxWidth(fillFraction),
  677. onBookmarkClicked = {
  678. onMultiBookmarkClicked.invoke(selected.fastMap { it.chapter }, true)
  679. }.takeIf { selected.fastAny { !it.chapter.bookmark } },
  680. onRemoveBookmarkClicked = {
  681. onMultiBookmarkClicked.invoke(selected.fastMap { it.chapter }, false)
  682. }.takeIf { selected.fastAll { it.chapter.bookmark } },
  683. onMarkAsReadClicked = {
  684. onMultiMarkAsReadClicked(selected.fastMap { it.chapter }, true)
  685. }.takeIf { selected.fastAny { !it.chapter.read } },
  686. onMarkAsUnreadClicked = {
  687. onMultiMarkAsReadClicked(selected.fastMap { it.chapter }, false)
  688. }.takeIf { selected.fastAny { it.chapter.read || it.chapter.lastPageRead > 0L } },
  689. onMarkPreviousAsReadClicked = {
  690. onMarkPreviousAsReadClicked(selected[0].chapter)
  691. }.takeIf { selected.size == 1 },
  692. onDownloadClicked = {
  693. onDownloadChapter!!(selected.toList(), ChapterDownloadAction.START)
  694. }.takeIf {
  695. onDownloadChapter != null && selected.fastAny { it.downloadState != Download.State.DOWNLOADED }
  696. },
  697. onDeleteClicked = {
  698. onMultiDeleteClicked(selected.fastMap { it.chapter })
  699. }.takeIf {
  700. selected.fastAny { it.downloadState == Download.State.DOWNLOADED }
  701. },
  702. )
  703. }
  704. private fun LazyListScope.sharedChapterItems(
  705. manga: Manga,
  706. chapters: List<ChapterList>,
  707. isAnyChapterSelected: Boolean,
  708. chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
  709. chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
  710. onChapterClicked: (Chapter) -> Unit,
  711. onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
  712. onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
  713. onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
  714. ) {
  715. items(
  716. items = chapters,
  717. key = { item ->
  718. when (item) {
  719. is ChapterList.MissingCount -> "missing-count-${item.id}"
  720. is ChapterList.Item -> "chapter-${item.id}"
  721. }
  722. },
  723. contentType = { MangaScreenItem.CHAPTER },
  724. ) { item ->
  725. val haptic = LocalHapticFeedback.current
  726. when (item) {
  727. is ChapterList.MissingCount -> {
  728. MissingChapterCountListItem(count = item.count)
  729. }
  730. is ChapterList.Item -> {
  731. MangaChapterListItem(
  732. title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) {
  733. stringResource(
  734. MR.strings.display_mode_chapter,
  735. formatChapterNumber(item.chapter.chapterNumber),
  736. )
  737. } else {
  738. item.chapter.name
  739. },
  740. date = relativeDateText(item.chapter.dateUpload),
  741. readProgress = item.chapter.lastPageRead
  742. .takeIf { !item.chapter.read && it > 0L }
  743. ?.let {
  744. stringResource(
  745. MR.strings.chapter_progress,
  746. it + 1,
  747. )
  748. },
  749. scanlator = item.chapter.scanlator.takeIf { !it.isNullOrBlank() },
  750. read = item.chapter.read,
  751. bookmark = item.chapter.bookmark,
  752. selected = item.selected,
  753. downloadIndicatorEnabled = !isAnyChapterSelected && !manga.isLocal(),
  754. downloadStateProvider = { item.downloadState },
  755. downloadProgressProvider = { item.downloadProgress },
  756. chapterSwipeStartAction = chapterSwipeStartAction,
  757. chapterSwipeEndAction = chapterSwipeEndAction,
  758. onLongClick = {
  759. onChapterSelected(item, !item.selected, true, true)
  760. haptic.performHapticFeedback(HapticFeedbackType.LongPress)
  761. },
  762. onClick = {
  763. onChapterItemClick(
  764. chapterItem = item,
  765. isAnyChapterSelected = isAnyChapterSelected,
  766. onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
  767. onChapterClicked = onChapterClicked,
  768. )
  769. },
  770. onDownloadClick = if (onDownloadChapter != null) {
  771. { onDownloadChapter(listOf(item), it) }
  772. } else {
  773. null
  774. },
  775. onChapterSwipe = {
  776. onChapterSwipe(item, it)
  777. },
  778. )
  779. }
  780. }
  781. }
  782. }
  783. private fun onChapterItemClick(
  784. chapterItem: ChapterList.Item,
  785. isAnyChapterSelected: Boolean,
  786. onToggleSelection: (Boolean) -> Unit,
  787. onChapterClicked: (Chapter) -> Unit,
  788. ) {
  789. when {
  790. chapterItem.selected -> onToggleSelection(false)
  791. isAnyChapterSelected -> onToggleSelection(true)
  792. else -> onChapterClicked(chapterItem.chapter)
  793. }
  794. }