MangaScreen.kt 35 KB

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