MangaScreen.kt 35 KB

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