MangaScreen.kt 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  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.Row
  11. import androidx.compose.foundation.layout.WindowInsets
  12. import androidx.compose.foundation.layout.WindowInsetsSides
  13. import androidx.compose.foundation.layout.asPaddingValues
  14. import androidx.compose.foundation.layout.calculateEndPadding
  15. import androidx.compose.foundation.layout.calculateStartPadding
  16. import androidx.compose.foundation.layout.fillMaxHeight
  17. import androidx.compose.foundation.layout.fillMaxWidth
  18. import androidx.compose.foundation.layout.navigationBars
  19. import androidx.compose.foundation.layout.only
  20. import androidx.compose.foundation.layout.padding
  21. import androidx.compose.foundation.layout.systemBars
  22. import androidx.compose.foundation.layout.widthIn
  23. import androidx.compose.foundation.lazy.LazyListScope
  24. import androidx.compose.foundation.lazy.items
  25. import androidx.compose.foundation.lazy.rememberLazyListState
  26. import androidx.compose.foundation.rememberScrollState
  27. import androidx.compose.foundation.verticalScroll
  28. import androidx.compose.material.icons.Icons
  29. import androidx.compose.material.icons.filled.PlayArrow
  30. import androidx.compose.material3.Icon
  31. import androidx.compose.material3.SnackbarHost
  32. import androidx.compose.material3.SnackbarHostState
  33. import androidx.compose.material3.Text
  34. import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
  35. import androidx.compose.runtime.Composable
  36. import androidx.compose.runtime.derivedStateOf
  37. import androidx.compose.runtime.getValue
  38. import androidx.compose.runtime.mutableStateOf
  39. import androidx.compose.runtime.remember
  40. import androidx.compose.runtime.snapshots.SnapshotStateList
  41. import androidx.compose.runtime.toMutableStateList
  42. import androidx.compose.ui.Alignment
  43. import androidx.compose.ui.Modifier
  44. import androidx.compose.ui.hapticfeedback.HapticFeedbackType
  45. import androidx.compose.ui.layout.onSizeChanged
  46. import androidx.compose.ui.platform.LocalDensity
  47. import androidx.compose.ui.platform.LocalHapticFeedback
  48. import androidx.compose.ui.platform.LocalLayoutDirection
  49. import androidx.compose.ui.res.stringResource
  50. import androidx.compose.ui.unit.dp
  51. import com.google.accompanist.swiperefresh.SwipeRefresh
  52. import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
  53. import eu.kanade.domain.chapter.model.Chapter
  54. import eu.kanade.presentation.components.ExtendedFloatingActionButton
  55. import eu.kanade.presentation.components.LazyColumn
  56. import eu.kanade.presentation.components.Scaffold
  57. import eu.kanade.presentation.components.SwipeRefreshIndicator
  58. import eu.kanade.presentation.components.VerticalFastScroller
  59. import eu.kanade.presentation.manga.components.ChapterHeader
  60. import eu.kanade.presentation.manga.components.ExpandableMangaDescription
  61. import eu.kanade.presentation.manga.components.MangaActionRow
  62. import eu.kanade.presentation.manga.components.MangaBottomActionMenu
  63. import eu.kanade.presentation.manga.components.MangaChapterListItem
  64. import eu.kanade.presentation.manga.components.MangaInfoBox
  65. import eu.kanade.presentation.manga.components.MangaSmallAppBar
  66. import eu.kanade.presentation.util.isScrolledToEnd
  67. import eu.kanade.presentation.util.isScrollingUp
  68. import eu.kanade.presentation.util.plus
  69. import eu.kanade.tachiyomi.R
  70. import eu.kanade.tachiyomi.data.download.model.Download
  71. import eu.kanade.tachiyomi.source.SourceManager
  72. import eu.kanade.tachiyomi.source.getNameForMangaInfo
  73. import eu.kanade.tachiyomi.ui.manga.ChapterItem
  74. import eu.kanade.tachiyomi.ui.manga.MangaScreenState
  75. @Composable
  76. fun MangaScreen(
  77. state: MangaScreenState.Success,
  78. snackbarHostState: SnackbarHostState,
  79. windowWidthSizeClass: WindowWidthSizeClass,
  80. onBackClicked: () -> Unit,
  81. onChapterClicked: (Chapter) -> Unit,
  82. onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
  83. onAddToLibraryClicked: () -> Unit,
  84. onWebViewClicked: (() -> Unit)?,
  85. onTrackingClicked: (() -> Unit)?,
  86. onTagClicked: (String) -> Unit,
  87. onFilterButtonClicked: () -> Unit,
  88. onRefresh: () -> Unit,
  89. onContinueReading: () -> Unit,
  90. onSearch: (query: String, global: Boolean) -> Unit,
  91. // For cover dialog
  92. onCoverClicked: () -> Unit,
  93. // For top action menu
  94. onShareClicked: (() -> Unit)?,
  95. onDownloadActionClicked: ((DownloadAction) -> Unit)?,
  96. onEditCategoryClicked: (() -> Unit)?,
  97. onMigrateClicked: (() -> Unit)?,
  98. // For bottom action menu
  99. onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
  100. onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
  101. onMarkPreviousAsReadClicked: (Chapter) -> Unit,
  102. onMultiDeleteClicked: (List<Chapter>) -> Unit,
  103. ) {
  104. if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
  105. MangaScreenSmallImpl(
  106. state = state,
  107. snackbarHostState = snackbarHostState,
  108. onBackClicked = onBackClicked,
  109. onChapterClicked = onChapterClicked,
  110. onDownloadChapter = onDownloadChapter,
  111. onAddToLibraryClicked = onAddToLibraryClicked,
  112. onWebViewClicked = onWebViewClicked,
  113. onTrackingClicked = onTrackingClicked,
  114. onTagClicked = onTagClicked,
  115. onFilterButtonClicked = onFilterButtonClicked,
  116. onRefresh = onRefresh,
  117. onContinueReading = onContinueReading,
  118. onSearch = onSearch,
  119. onCoverClicked = onCoverClicked,
  120. onShareClicked = onShareClicked,
  121. onDownloadActionClicked = onDownloadActionClicked,
  122. onEditCategoryClicked = onEditCategoryClicked,
  123. onMigrateClicked = onMigrateClicked,
  124. onMultiBookmarkClicked = onMultiBookmarkClicked,
  125. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  126. onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
  127. onMultiDeleteClicked = onMultiDeleteClicked,
  128. )
  129. } else {
  130. MangaScreenLargeImpl(
  131. state = state,
  132. windowWidthSizeClass = windowWidthSizeClass,
  133. snackbarHostState = snackbarHostState,
  134. onBackClicked = onBackClicked,
  135. onChapterClicked = onChapterClicked,
  136. onDownloadChapter = onDownloadChapter,
  137. onAddToLibraryClicked = onAddToLibraryClicked,
  138. onWebViewClicked = onWebViewClicked,
  139. onTrackingClicked = onTrackingClicked,
  140. onTagClicked = onTagClicked,
  141. onFilterButtonClicked = onFilterButtonClicked,
  142. onRefresh = onRefresh,
  143. onContinueReading = onContinueReading,
  144. onSearch = onSearch,
  145. onCoverClicked = onCoverClicked,
  146. onShareClicked = onShareClicked,
  147. onDownloadActionClicked = onDownloadActionClicked,
  148. onEditCategoryClicked = onEditCategoryClicked,
  149. onMigrateClicked = onMigrateClicked,
  150. onMultiBookmarkClicked = onMultiBookmarkClicked,
  151. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  152. onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
  153. onMultiDeleteClicked = onMultiDeleteClicked,
  154. )
  155. }
  156. }
  157. @Composable
  158. private fun MangaScreenSmallImpl(
  159. state: MangaScreenState.Success,
  160. snackbarHostState: SnackbarHostState,
  161. onBackClicked: () -> Unit,
  162. onChapterClicked: (Chapter) -> Unit,
  163. onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
  164. onAddToLibraryClicked: () -> Unit,
  165. onWebViewClicked: (() -> Unit)?,
  166. onTrackingClicked: (() -> Unit)?,
  167. onTagClicked: (String) -> Unit,
  168. onFilterButtonClicked: () -> Unit,
  169. onRefresh: () -> Unit,
  170. onContinueReading: () -> Unit,
  171. onSearch: (query: String, global: Boolean) -> Unit,
  172. // For cover dialog
  173. onCoverClicked: () -> Unit,
  174. // For top action menu
  175. onShareClicked: (() -> Unit)?,
  176. onDownloadActionClicked: ((DownloadAction) -> Unit)?,
  177. onEditCategoryClicked: (() -> Unit)?,
  178. onMigrateClicked: (() -> Unit)?,
  179. // For bottom action menu
  180. onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
  181. onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
  182. onMarkPreviousAsReadClicked: (Chapter) -> Unit,
  183. onMultiDeleteClicked: (List<Chapter>) -> Unit,
  184. ) {
  185. val layoutDirection = LocalLayoutDirection.current
  186. val chapterListState = rememberLazyListState()
  187. val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
  188. val chapters = remember(state) { state.processedChapters.toList() }
  189. val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() }
  190. val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list
  191. val internalOnBackPressed = {
  192. if (selected.isNotEmpty()) {
  193. selected.clear()
  194. } else {
  195. onBackClicked()
  196. }
  197. }
  198. BackHandler(onBack = internalOnBackPressed)
  199. Scaffold(
  200. modifier = Modifier
  201. .padding(insetPadding),
  202. topBar = {
  203. val firstVisibleItemIndex by remember {
  204. derivedStateOf { chapterListState.firstVisibleItemIndex }
  205. }
  206. val firstVisibleItemScrollOffset by remember {
  207. derivedStateOf { chapterListState.firstVisibleItemScrollOffset }
  208. }
  209. val animatedTitleAlpha by animateFloatAsState(
  210. if (firstVisibleItemIndex > 0) 1f else 0f,
  211. )
  212. val animatedBgAlpha by animateFloatAsState(
  213. if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
  214. )
  215. MangaSmallAppBar(
  216. title = state.manga.title,
  217. titleAlphaProvider = { animatedTitleAlpha },
  218. backgroundAlphaProvider = { animatedBgAlpha },
  219. incognitoMode = state.isIncognitoMode,
  220. downloadedOnlyMode = state.isDownloadedOnlyMode,
  221. onBackClicked = internalOnBackPressed,
  222. onShareClicked = onShareClicked,
  223. onDownloadClicked = onDownloadActionClicked,
  224. onEditCategoryClicked = onEditCategoryClicked,
  225. onMigrateClicked = onMigrateClicked,
  226. actionModeCounter = selected.size,
  227. onSelectAll = {
  228. selected.clear()
  229. selected.addAll(chapters)
  230. },
  231. onInvertSelection = {
  232. val toSelect = chapters - selected
  233. selected.clear()
  234. selected.addAll(toSelect)
  235. },
  236. )
  237. },
  238. bottomBar = {
  239. SharedMangaBottomActionMenu(
  240. selected = selected,
  241. onMultiBookmarkClicked = onMultiBookmarkClicked,
  242. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  243. onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
  244. onDownloadChapter = onDownloadChapter,
  245. onMultiDeleteClicked = onMultiDeleteClicked,
  246. fillFraction = 1f,
  247. )
  248. },
  249. snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
  250. floatingActionButton = {
  251. AnimatedVisibility(
  252. visible = chapters.any { !it.chapter.read } && selected.isEmpty(),
  253. enter = fadeIn(),
  254. exit = fadeOut(),
  255. ) {
  256. ExtendedFloatingActionButton(
  257. text = {
  258. val id = if (chapters.any { it.chapter.read }) {
  259. R.string.action_resume
  260. } else {
  261. R.string.action_start
  262. }
  263. Text(text = stringResource(id))
  264. },
  265. icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
  266. onClick = onContinueReading,
  267. expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
  268. modifier = Modifier
  269. .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
  270. )
  271. }
  272. },
  273. ) { contentPadding ->
  274. val noTopContentPadding = PaddingValues(
  275. start = contentPadding.calculateStartPadding(layoutDirection),
  276. end = contentPadding.calculateEndPadding(layoutDirection),
  277. bottom = contentPadding.calculateBottomPadding(),
  278. ) + WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
  279. val topPadding = contentPadding.calculateTopPadding()
  280. SwipeRefresh(
  281. state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter),
  282. onRefresh = onRefresh,
  283. indicatorPadding = contentPadding,
  284. indicator = { s, trigger ->
  285. SwipeRefreshIndicator(
  286. state = s,
  287. refreshTriggerDistance = trigger,
  288. )
  289. },
  290. ) {
  291. VerticalFastScroller(
  292. listState = chapterListState,
  293. topContentPadding = topPadding,
  294. endContentPadding = noTopContentPadding.calculateEndPadding(layoutDirection),
  295. ) {
  296. LazyColumn(
  297. modifier = Modifier.fillMaxHeight(),
  298. state = chapterListState,
  299. contentPadding = noTopContentPadding,
  300. ) {
  301. item(
  302. key = MangaScreenItem.INFO_BOX,
  303. contentType = MangaScreenItem.INFO_BOX,
  304. ) {
  305. MangaInfoBox(
  306. windowWidthSizeClass = WindowWidthSizeClass.Compact,
  307. appBarPadding = topPadding,
  308. title = state.manga.title,
  309. author = state.manga.author,
  310. artist = state.manga.artist,
  311. sourceName = remember { state.source.getNameForMangaInfo() },
  312. isStubSource = remember { state.source is SourceManager.StubSource },
  313. coverDataProvider = { state.manga },
  314. status = state.manga.status,
  315. onCoverClick = onCoverClicked,
  316. doSearch = onSearch,
  317. )
  318. }
  319. item(
  320. key = MangaScreenItem.ACTION_ROW,
  321. contentType = MangaScreenItem.ACTION_ROW,
  322. ) {
  323. MangaActionRow(
  324. favorite = state.manga.favorite,
  325. trackingCount = state.trackingCount,
  326. onAddToLibraryClicked = onAddToLibraryClicked,
  327. onWebViewClicked = onWebViewClicked,
  328. onTrackingClicked = onTrackingClicked,
  329. onEditCategory = onEditCategoryClicked,
  330. )
  331. }
  332. item(
  333. key = MangaScreenItem.DESCRIPTION_WITH_TAG,
  334. contentType = MangaScreenItem.DESCRIPTION_WITH_TAG,
  335. ) {
  336. ExpandableMangaDescription(
  337. defaultExpandState = state.isFromSource,
  338. description = state.manga.description,
  339. tagsProvider = { state.manga.genre },
  340. onTagClicked = onTagClicked,
  341. )
  342. }
  343. item(
  344. key = MangaScreenItem.CHAPTER_HEADER,
  345. contentType = MangaScreenItem.CHAPTER_HEADER,
  346. ) {
  347. ChapterHeader(
  348. chapterCount = chapters.size,
  349. isChapterFiltered = state.manga.chaptersFiltered(),
  350. onFilterButtonClicked = onFilterButtonClicked,
  351. )
  352. }
  353. sharedChapterItems(
  354. chapters = chapters,
  355. selected = selected,
  356. selectedPositions = selectedPositions,
  357. onChapterClicked = onChapterClicked,
  358. onDownloadChapter = onDownloadChapter,
  359. )
  360. }
  361. }
  362. }
  363. }
  364. }
  365. @Composable
  366. fun MangaScreenLargeImpl(
  367. state: MangaScreenState.Success,
  368. windowWidthSizeClass: WindowWidthSizeClass,
  369. snackbarHostState: SnackbarHostState,
  370. onBackClicked: () -> Unit,
  371. onChapterClicked: (Chapter) -> Unit,
  372. onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
  373. onAddToLibraryClicked: () -> Unit,
  374. onWebViewClicked: (() -> Unit)?,
  375. onTrackingClicked: (() -> Unit)?,
  376. onTagClicked: (String) -> Unit,
  377. onFilterButtonClicked: () -> Unit,
  378. onRefresh: () -> Unit,
  379. onContinueReading: () -> Unit,
  380. onSearch: (query: String, global: Boolean) -> Unit,
  381. // For cover dialog
  382. onCoverClicked: () -> Unit,
  383. // For top action menu
  384. onShareClicked: (() -> Unit)?,
  385. onDownloadActionClicked: ((DownloadAction) -> Unit)?,
  386. onEditCategoryClicked: (() -> Unit)?,
  387. onMigrateClicked: (() -> Unit)?,
  388. // For bottom action menu
  389. onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
  390. onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
  391. onMarkPreviousAsReadClicked: (Chapter) -> Unit,
  392. onMultiDeleteClicked: (List<Chapter>) -> Unit,
  393. ) {
  394. val layoutDirection = LocalLayoutDirection.current
  395. val density = LocalDensity.current
  396. val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
  397. val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) }
  398. SwipeRefresh(
  399. state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter),
  400. onRefresh = onRefresh,
  401. indicatorPadding = PaddingValues(
  402. start = insetPadding.calculateStartPadding(layoutDirection),
  403. top = with(density) { topBarHeight.toDp() },
  404. end = insetPadding.calculateEndPadding(layoutDirection),
  405. ),
  406. clipIndicatorToPadding = true,
  407. indicator = { s, trigger ->
  408. SwipeRefreshIndicator(
  409. state = s,
  410. refreshTriggerDistance = trigger,
  411. )
  412. },
  413. ) {
  414. val chapterListState = rememberLazyListState()
  415. val chapters = remember(state) { state.processedChapters.toList() }
  416. val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() }
  417. val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list
  418. val internalOnBackPressed = {
  419. if (selected.isNotEmpty()) {
  420. selected.clear()
  421. } else {
  422. onBackClicked()
  423. }
  424. }
  425. BackHandler(onBack = internalOnBackPressed)
  426. Scaffold(
  427. modifier = Modifier.padding(insetPadding),
  428. topBar = {
  429. MangaSmallAppBar(
  430. modifier = Modifier.onSizeChanged { onTopBarHeightChanged(it.height) },
  431. title = state.manga.title,
  432. titleAlphaProvider = { if (selected.isEmpty()) 0f else 1f },
  433. backgroundAlphaProvider = { 1f },
  434. incognitoMode = state.isIncognitoMode,
  435. downloadedOnlyMode = state.isDownloadedOnlyMode,
  436. onBackClicked = internalOnBackPressed,
  437. onShareClicked = onShareClicked,
  438. onDownloadClicked = onDownloadActionClicked,
  439. onEditCategoryClicked = onEditCategoryClicked,
  440. onMigrateClicked = onMigrateClicked,
  441. actionModeCounter = selected.size,
  442. onSelectAll = {
  443. selected.clear()
  444. selected.addAll(chapters)
  445. },
  446. onInvertSelection = {
  447. val toSelect = chapters - selected
  448. selected.clear()
  449. selected.addAll(toSelect)
  450. },
  451. )
  452. },
  453. bottomBar = {
  454. Box(
  455. modifier = Modifier.fillMaxWidth(),
  456. contentAlignment = Alignment.BottomEnd,
  457. ) {
  458. SharedMangaBottomActionMenu(
  459. selected = selected,
  460. onMultiBookmarkClicked = onMultiBookmarkClicked,
  461. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  462. onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
  463. onDownloadChapter = onDownloadChapter,
  464. onMultiDeleteClicked = onMultiDeleteClicked,
  465. fillFraction = 0.5f,
  466. )
  467. }
  468. },
  469. snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
  470. floatingActionButton = {
  471. AnimatedVisibility(
  472. visible = chapters.any { !it.chapter.read } && selected.isEmpty(),
  473. enter = fadeIn(),
  474. exit = fadeOut(),
  475. ) {
  476. ExtendedFloatingActionButton(
  477. text = {
  478. val id = if (chapters.any { it.chapter.read }) {
  479. R.string.action_resume
  480. } else {
  481. R.string.action_start
  482. }
  483. Text(text = stringResource(id))
  484. },
  485. icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
  486. onClick = onContinueReading,
  487. expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
  488. modifier = Modifier
  489. .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
  490. )
  491. }
  492. },
  493. ) { contentPadding ->
  494. Row {
  495. val withNavBarContentPadding = contentPadding +
  496. WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
  497. Column(
  498. modifier = Modifier
  499. .widthIn(max = 450.dp)
  500. .verticalScroll(rememberScrollState())
  501. .padding(bottom = withNavBarContentPadding.calculateBottomPadding()),
  502. ) {
  503. MangaInfoBox(
  504. windowWidthSizeClass = windowWidthSizeClass,
  505. appBarPadding = contentPadding.calculateTopPadding(),
  506. title = state.manga.title,
  507. author = state.manga.author,
  508. artist = state.manga.artist,
  509. sourceName = remember { state.source.getNameForMangaInfo() },
  510. isStubSource = remember { state.source is SourceManager.StubSource },
  511. coverDataProvider = { state.manga },
  512. status = state.manga.status,
  513. onCoverClick = onCoverClicked,
  514. doSearch = onSearch,
  515. )
  516. MangaActionRow(
  517. favorite = state.manga.favorite,
  518. trackingCount = state.trackingCount,
  519. onAddToLibraryClicked = onAddToLibraryClicked,
  520. onWebViewClicked = onWebViewClicked,
  521. onTrackingClicked = onTrackingClicked,
  522. onEditCategory = onEditCategoryClicked,
  523. )
  524. ExpandableMangaDescription(
  525. defaultExpandState = true,
  526. description = state.manga.description,
  527. tagsProvider = { state.manga.genre },
  528. onTagClicked = onTagClicked,
  529. )
  530. }
  531. VerticalFastScroller(
  532. listState = chapterListState,
  533. modifier = Modifier.weight(1f),
  534. topContentPadding = withNavBarContentPadding.calculateTopPadding(),
  535. endContentPadding = withNavBarContentPadding.calculateEndPadding(layoutDirection),
  536. ) {
  537. LazyColumn(
  538. modifier = Modifier.fillMaxHeight(),
  539. state = chapterListState,
  540. contentPadding = withNavBarContentPadding,
  541. ) {
  542. item(
  543. key = MangaScreenItem.CHAPTER_HEADER,
  544. contentType = MangaScreenItem.CHAPTER_HEADER,
  545. ) {
  546. ChapterHeader(
  547. chapterCount = chapters.size,
  548. isChapterFiltered = state.manga.chaptersFiltered(),
  549. onFilterButtonClicked = onFilterButtonClicked,
  550. )
  551. }
  552. sharedChapterItems(
  553. chapters = chapters,
  554. selected = selected,
  555. selectedPositions = selectedPositions,
  556. onChapterClicked = onChapterClicked,
  557. onDownloadChapter = onDownloadChapter,
  558. )
  559. }
  560. }
  561. }
  562. }
  563. }
  564. }
  565. @Composable
  566. private fun SharedMangaBottomActionMenu(
  567. selected: SnapshotStateList<ChapterItem>,
  568. onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
  569. onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
  570. onMarkPreviousAsReadClicked: (Chapter) -> Unit,
  571. onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
  572. onMultiDeleteClicked: (List<Chapter>) -> Unit,
  573. fillFraction: Float,
  574. ) {
  575. MangaBottomActionMenu(
  576. visible = selected.isNotEmpty(),
  577. modifier = Modifier.fillMaxWidth(fillFraction),
  578. onBookmarkClicked = {
  579. onMultiBookmarkClicked.invoke(selected.map { it.chapter }, true)
  580. selected.clear()
  581. }.takeIf { selected.any { !it.chapter.bookmark } },
  582. onRemoveBookmarkClicked = {
  583. onMultiBookmarkClicked.invoke(selected.map { it.chapter }, false)
  584. selected.clear()
  585. }.takeIf { selected.all { it.chapter.bookmark } },
  586. onMarkAsReadClicked = {
  587. onMultiMarkAsReadClicked(selected.map { it.chapter }, true)
  588. selected.clear()
  589. }.takeIf { selected.any { !it.chapter.read } },
  590. onMarkAsUnreadClicked = {
  591. onMultiMarkAsReadClicked(selected.map { it.chapter }, false)
  592. selected.clear()
  593. }.takeIf { selected.any { it.chapter.read } },
  594. onMarkPreviousAsReadClicked = {
  595. onMarkPreviousAsReadClicked(selected[0].chapter)
  596. selected.clear()
  597. }.takeIf { selected.size == 1 },
  598. onDownloadClicked = {
  599. onDownloadChapter!!(selected.toList(), ChapterDownloadAction.START)
  600. selected.clear()
  601. }.takeIf {
  602. onDownloadChapter != null && selected.any { it.downloadState != Download.State.DOWNLOADED }
  603. },
  604. onDeleteClicked = {
  605. onMultiDeleteClicked(selected.map { it.chapter })
  606. selected.clear()
  607. }.takeIf {
  608. onDownloadChapter != null && selected.any { it.downloadState == Download.State.DOWNLOADED }
  609. },
  610. )
  611. }
  612. private fun LazyListScope.sharedChapterItems(
  613. chapters: List<ChapterItem>,
  614. selected: SnapshotStateList<ChapterItem>,
  615. selectedPositions: Array<Int>,
  616. onChapterClicked: (Chapter) -> Unit,
  617. onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
  618. ) {
  619. items(
  620. items = chapters,
  621. key = { it.chapter.id },
  622. contentType = { MangaScreenItem.CHAPTER },
  623. ) { chapterItem ->
  624. val haptic = LocalHapticFeedback.current
  625. MangaChapterListItem(
  626. title = chapterItem.chapterTitleString,
  627. date = chapterItem.dateUploadString,
  628. readProgress = chapterItem.readProgressString,
  629. scanlator = chapterItem.chapter.scanlator.takeIf { !it.isNullOrBlank() },
  630. read = chapterItem.chapter.read,
  631. bookmark = chapterItem.chapter.bookmark,
  632. selected = selected.contains(chapterItem),
  633. downloadStateProvider = { chapterItem.downloadState },
  634. downloadProgressProvider = { chapterItem.downloadProgress },
  635. onLongClick = {
  636. val dispatched = onChapterItemLongClick(
  637. chapterItem = chapterItem,
  638. selected = selected,
  639. chapters = chapters,
  640. selectedPositions = selectedPositions,
  641. )
  642. if (dispatched) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
  643. },
  644. onClick = {
  645. onChapterItemClick(
  646. chapterItem = chapterItem,
  647. selected = selected,
  648. chapters = chapters,
  649. selectedPositions = selectedPositions,
  650. onChapterClicked = onChapterClicked,
  651. )
  652. },
  653. onDownloadClick = if (onDownloadChapter != null) {
  654. { onDownloadChapter(listOf(chapterItem), it) }
  655. } else null,
  656. )
  657. }
  658. }
  659. private fun onChapterItemLongClick(
  660. chapterItem: ChapterItem,
  661. selected: MutableList<ChapterItem>,
  662. chapters: List<ChapterItem>,
  663. selectedPositions: Array<Int>,
  664. ): Boolean {
  665. if (!selected.contains(chapterItem)) {
  666. val selectedIndex = chapters.indexOf(chapterItem)
  667. if (selected.isEmpty()) {
  668. selected.add(chapterItem)
  669. selectedPositions[0] = selectedIndex
  670. selectedPositions[1] = selectedIndex
  671. return true
  672. }
  673. // Try to select the items in-between when possible
  674. val range: IntRange
  675. if (selectedIndex < selectedPositions[0]) {
  676. range = selectedIndex until selectedPositions[0]
  677. selectedPositions[0] = selectedIndex
  678. } else if (selectedIndex > selectedPositions[1]) {
  679. range = (selectedPositions[1] + 1)..selectedIndex
  680. selectedPositions[1] = selectedIndex
  681. } else {
  682. // Just select itself
  683. range = selectedIndex..selectedIndex
  684. }
  685. range.forEach {
  686. val toAdd = chapters[it]
  687. if (!selected.contains(toAdd)) {
  688. selected.add(toAdd)
  689. }
  690. }
  691. return true
  692. }
  693. return false
  694. }
  695. private fun onChapterItemClick(
  696. chapterItem: ChapterItem,
  697. selected: MutableList<ChapterItem>,
  698. chapters: List<ChapterItem>,
  699. selectedPositions: Array<Int>,
  700. onChapterClicked: (Chapter) -> Unit,
  701. ) {
  702. val selectedIndex = chapters.indexOf(chapterItem)
  703. when {
  704. selected.contains(chapterItem) -> {
  705. val removedIndex = chapters.indexOf(chapterItem)
  706. selected.remove(chapterItem)
  707. if (removedIndex == selectedPositions[0]) {
  708. selectedPositions[0] = chapters.indexOfFirst { selected.contains(it) }
  709. } else if (removedIndex == selectedPositions[1]) {
  710. selectedPositions[1] = chapters.indexOfLast { selected.contains(it) }
  711. }
  712. }
  713. selected.isNotEmpty() -> {
  714. if (selectedIndex < selectedPositions[0]) {
  715. selectedPositions[0] = selectedIndex
  716. } else if (selectedIndex > selectedPositions[1]) {
  717. selectedPositions[1] = selectedIndex
  718. }
  719. selected.add(chapterItem)
  720. }
  721. else -> onChapterClicked(chapterItem.chapter)
  722. }
  723. }