MangaScreen.kt 37 KB

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