MangaSettingsDialog.kt 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. package eu.kanade.presentation.manga
  2. import androidx.compose.foundation.clickable
  3. import androidx.compose.foundation.layout.Arrangement
  4. import androidx.compose.foundation.layout.Box
  5. import androidx.compose.foundation.layout.Column
  6. import androidx.compose.foundation.layout.PaddingValues
  7. import androidx.compose.foundation.layout.Row
  8. import androidx.compose.foundation.layout.Spacer
  9. import androidx.compose.foundation.layout.fillMaxWidth
  10. import androidx.compose.foundation.layout.heightIn
  11. import androidx.compose.foundation.layout.padding
  12. import androidx.compose.foundation.layout.size
  13. import androidx.compose.foundation.layout.wrapContentSize
  14. import androidx.compose.foundation.rememberScrollState
  15. import androidx.compose.foundation.verticalScroll
  16. import androidx.compose.material.icons.Icons
  17. import androidx.compose.material.icons.filled.ArrowDownward
  18. import androidx.compose.material.icons.filled.ArrowUpward
  19. import androidx.compose.material.icons.filled.MoreVert
  20. import androidx.compose.material.icons.rounded.CheckBox
  21. import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
  22. import androidx.compose.material.icons.rounded.DisabledByDefault
  23. import androidx.compose.material3.AlertDialog
  24. import androidx.compose.material3.Checkbox
  25. import androidx.compose.material3.DropdownMenuItem
  26. import androidx.compose.material3.Icon
  27. import androidx.compose.material3.IconButton
  28. import androidx.compose.material3.MaterialTheme
  29. import androidx.compose.material3.RadioButton
  30. import androidx.compose.material3.Surface
  31. import androidx.compose.material3.Tab
  32. import androidx.compose.material3.TabRow
  33. import androidx.compose.material3.Text
  34. import androidx.compose.material3.TextButton
  35. import androidx.compose.runtime.Composable
  36. import androidx.compose.runtime.getValue
  37. import androidx.compose.runtime.mutableStateOf
  38. import androidx.compose.runtime.remember
  39. import androidx.compose.runtime.rememberCoroutineScope
  40. import androidx.compose.runtime.saveable.rememberSaveable
  41. import androidx.compose.runtime.setValue
  42. import androidx.compose.ui.Alignment
  43. import androidx.compose.ui.Modifier
  44. import androidx.compose.ui.graphics.vector.ImageVector
  45. import androidx.compose.ui.layout.onSizeChanged
  46. import androidx.compose.ui.platform.LocalDensity
  47. import androidx.compose.ui.res.stringResource
  48. import androidx.compose.ui.unit.dp
  49. import androidx.compose.ui.util.fastForEachIndexed
  50. import eu.kanade.domain.manga.model.Manga
  51. import eu.kanade.domain.manga.model.TriStateFilter
  52. import eu.kanade.presentation.components.AdaptiveSheet
  53. import eu.kanade.presentation.components.Divider
  54. import eu.kanade.presentation.components.DropdownMenu
  55. import eu.kanade.presentation.components.HorizontalPager
  56. import eu.kanade.presentation.components.TabIndicator
  57. import eu.kanade.presentation.components.rememberPagerState
  58. import eu.kanade.presentation.theme.TachiyomiTheme
  59. import eu.kanade.presentation.util.ThemePreviews
  60. import eu.kanade.tachiyomi.R
  61. import kotlinx.coroutines.launch
  62. @Composable
  63. fun ChapterSettingsDialog(
  64. onDismissRequest: () -> Unit,
  65. manga: Manga? = null,
  66. onDownloadFilterChanged: (TriStateFilter) -> Unit,
  67. onUnreadFilterChanged: (TriStateFilter) -> Unit,
  68. onBookmarkedFilterChanged: (TriStateFilter) -> Unit,
  69. onSortModeChanged: (Long) -> Unit,
  70. onDisplayModeChanged: (Long) -> Unit,
  71. onSetAsDefault: (applyToExistingManga: Boolean) -> Unit,
  72. ) {
  73. AdaptiveSheet(
  74. onDismissRequest = onDismissRequest,
  75. ) { contentPadding ->
  76. ChapterSettingsDialogImpl(
  77. manga = manga,
  78. contentPadding = contentPadding,
  79. onDownloadFilterChanged = onDownloadFilterChanged,
  80. onUnreadFilterChanged = onUnreadFilterChanged,
  81. onBookmarkedFilterChanged = onBookmarkedFilterChanged,
  82. onSortModeChanged = onSortModeChanged,
  83. onDisplayModeChanged = onDisplayModeChanged,
  84. onSetAsDefault = onSetAsDefault,
  85. )
  86. }
  87. }
  88. @Composable
  89. private fun ChapterSettingsDialogImpl(
  90. manga: Manga? = null,
  91. contentPadding: PaddingValues = PaddingValues(),
  92. onDownloadFilterChanged: (TriStateFilter) -> Unit,
  93. onUnreadFilterChanged: (TriStateFilter) -> Unit,
  94. onBookmarkedFilterChanged: (TriStateFilter) -> Unit,
  95. onSortModeChanged: (Long) -> Unit,
  96. onDisplayModeChanged: (Long) -> Unit,
  97. onSetAsDefault: (applyToExistingManga: Boolean) -> Unit,
  98. ) {
  99. val scope = rememberCoroutineScope()
  100. val tabTitles = listOf(
  101. stringResource(R.string.action_filter),
  102. stringResource(R.string.action_sort),
  103. stringResource(R.string.action_display),
  104. )
  105. val pagerState = rememberPagerState()
  106. var showSetAsDefaultDialog by rememberSaveable { mutableStateOf(false) }
  107. if (showSetAsDefaultDialog) {
  108. SetAsDefaultDialog(
  109. onDismissRequest = { showSetAsDefaultDialog = false },
  110. onConfirmed = onSetAsDefault,
  111. )
  112. }
  113. Column {
  114. Row {
  115. TabRow(
  116. modifier = Modifier.weight(1f),
  117. selectedTabIndex = pagerState.currentPage,
  118. indicator = { TabIndicator(it[pagerState.currentPage]) },
  119. divider = {},
  120. ) {
  121. tabTitles.fastForEachIndexed { i, s ->
  122. val selected = pagerState.currentPage == i
  123. Tab(
  124. selected = selected,
  125. onClick = { scope.launch { pagerState.animateScrollToPage(i) } },
  126. text = {
  127. Text(
  128. text = s,
  129. color = if (selected) {
  130. MaterialTheme.colorScheme.primary
  131. } else {
  132. MaterialTheme.colorScheme.onSurfaceVariant
  133. },
  134. )
  135. },
  136. )
  137. }
  138. }
  139. MoreMenu(onSetAsDefault = { showSetAsDefaultDialog = true })
  140. }
  141. Divider()
  142. val density = LocalDensity.current
  143. var largestHeight by rememberSaveable { mutableStateOf(0f) }
  144. HorizontalPager(
  145. modifier = Modifier.heightIn(min = largestHeight.dp),
  146. count = tabTitles.size,
  147. state = pagerState,
  148. verticalAlignment = Alignment.Top,
  149. ) { page ->
  150. Box(
  151. modifier = Modifier.onSizeChanged {
  152. with(density) {
  153. val heightDp = it.height.toDp()
  154. if (heightDp.value > largestHeight) {
  155. largestHeight = heightDp.value
  156. }
  157. }
  158. },
  159. ) {
  160. when (page) {
  161. 0 -> {
  162. val forceDownloaded = manga?.forceDownloaded() == true
  163. FilterPage(
  164. contentPadding = contentPadding,
  165. downloadFilter = if (forceDownloaded) {
  166. TriStateFilter.ENABLED_NOT
  167. } else {
  168. manga?.downloadedFilter
  169. } ?: TriStateFilter.DISABLED,
  170. onDownloadFilterChanged = onDownloadFilterChanged.takeUnless { forceDownloaded },
  171. unreadFilter = manga?.unreadFilter ?: TriStateFilter.DISABLED,
  172. onUnreadFilterChanged = onUnreadFilterChanged,
  173. bookmarkedFilter = manga?.bookmarkedFilter ?: TriStateFilter.DISABLED,
  174. onBookmarkedFilterChanged = onBookmarkedFilterChanged,
  175. )
  176. }
  177. 1 -> SortPage(
  178. contentPadding = contentPadding,
  179. sortingMode = manga?.sorting ?: 0,
  180. sortDescending = manga?.sortDescending() ?: false,
  181. onItemSelected = onSortModeChanged,
  182. )
  183. 2 -> DisplayPage(
  184. contentPadding = contentPadding,
  185. displayMode = manga?.displayMode ?: 0,
  186. onItemSelected = onDisplayModeChanged,
  187. )
  188. }
  189. }
  190. }
  191. }
  192. }
  193. @Composable
  194. private fun SetAsDefaultDialog(
  195. onDismissRequest: () -> Unit,
  196. onConfirmed: (optionalChecked: Boolean) -> Unit,
  197. ) {
  198. var optionalChecked by rememberSaveable { mutableStateOf(false) }
  199. AlertDialog(
  200. onDismissRequest = onDismissRequest,
  201. title = { Text(text = stringResource(R.string.chapter_settings)) },
  202. text = {
  203. Column(
  204. verticalArrangement = Arrangement.spacedBy(12.dp),
  205. ) {
  206. Text(text = stringResource(R.string.confirm_set_chapter_settings))
  207. Row(
  208. modifier = Modifier
  209. .clickable { optionalChecked = !optionalChecked }
  210. .padding(vertical = 8.dp)
  211. .fillMaxWidth(),
  212. horizontalArrangement = Arrangement.spacedBy(12.dp),
  213. verticalAlignment = Alignment.CenterVertically,
  214. ) {
  215. Checkbox(
  216. checked = optionalChecked,
  217. onCheckedChange = null,
  218. )
  219. Text(text = stringResource(R.string.also_set_chapter_settings_for_library))
  220. }
  221. }
  222. },
  223. dismissButton = {
  224. TextButton(onClick = onDismissRequest) {
  225. Text(text = stringResource(android.R.string.cancel))
  226. }
  227. },
  228. confirmButton = {
  229. TextButton(
  230. onClick = {
  231. onConfirmed(optionalChecked)
  232. },
  233. ) {
  234. Text(text = stringResource(android.R.string.ok))
  235. }
  236. },
  237. )
  238. }
  239. @Composable
  240. private fun MoreMenu(
  241. onSetAsDefault: () -> Unit,
  242. ) {
  243. var expanded by remember { mutableStateOf(false) }
  244. Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
  245. IconButton(onClick = { expanded = true }) {
  246. Icon(
  247. imageVector = Icons.Default.MoreVert,
  248. contentDescription = stringResource(R.string.label_more),
  249. )
  250. }
  251. DropdownMenu(
  252. expanded = expanded,
  253. onDismissRequest = { expanded = false },
  254. ) {
  255. DropdownMenuItem(
  256. text = { Text(stringResource(R.string.set_chapter_settings_as_default)) },
  257. onClick = {
  258. onSetAsDefault()
  259. expanded = false
  260. },
  261. )
  262. }
  263. }
  264. }
  265. @Composable
  266. private fun FilterPage(
  267. contentPadding: PaddingValues,
  268. downloadFilter: TriStateFilter,
  269. onDownloadFilterChanged: ((TriStateFilter) -> Unit)?,
  270. unreadFilter: TriStateFilter,
  271. onUnreadFilterChanged: (TriStateFilter) -> Unit,
  272. bookmarkedFilter: TriStateFilter,
  273. onBookmarkedFilterChanged: (TriStateFilter) -> Unit,
  274. ) {
  275. Column(
  276. modifier = Modifier
  277. .padding(vertical = VerticalPadding)
  278. .padding(contentPadding)
  279. .verticalScroll(rememberScrollState()),
  280. ) {
  281. FilterPageItem(
  282. label = stringResource(R.string.label_downloaded),
  283. state = downloadFilter,
  284. onClick = onDownloadFilterChanged,
  285. )
  286. FilterPageItem(
  287. label = stringResource(R.string.action_filter_unread),
  288. state = unreadFilter,
  289. onClick = onUnreadFilterChanged,
  290. )
  291. FilterPageItem(
  292. label = stringResource(R.string.action_filter_bookmarked),
  293. state = bookmarkedFilter,
  294. onClick = onBookmarkedFilterChanged,
  295. )
  296. }
  297. }
  298. @Composable
  299. private fun FilterPageItem(
  300. label: String,
  301. state: TriStateFilter,
  302. onClick: ((TriStateFilter) -> Unit)?,
  303. ) {
  304. Row(
  305. modifier = Modifier
  306. .clickable(
  307. enabled = onClick != null,
  308. onClick = {
  309. when (state) {
  310. TriStateFilter.DISABLED -> onClick?.invoke(TriStateFilter.ENABLED_IS)
  311. TriStateFilter.ENABLED_IS -> onClick?.invoke(TriStateFilter.ENABLED_NOT)
  312. TriStateFilter.ENABLED_NOT -> onClick?.invoke(TriStateFilter.DISABLED)
  313. }
  314. },
  315. )
  316. .fillMaxWidth()
  317. .padding(horizontal = HorizontalPadding, vertical = 12.dp),
  318. verticalAlignment = Alignment.CenterVertically,
  319. horizontalArrangement = Arrangement.spacedBy(24.dp),
  320. ) {
  321. Icon(
  322. imageVector = when (state) {
  323. TriStateFilter.DISABLED -> Icons.Rounded.CheckBoxOutlineBlank
  324. TriStateFilter.ENABLED_IS -> Icons.Rounded.CheckBox
  325. TriStateFilter.ENABLED_NOT -> Icons.Rounded.DisabledByDefault
  326. },
  327. contentDescription = null,
  328. tint = if (state == TriStateFilter.DISABLED) {
  329. MaterialTheme.colorScheme.onSurfaceVariant
  330. } else {
  331. MaterialTheme.colorScheme.primary
  332. },
  333. )
  334. Text(
  335. text = label,
  336. style = MaterialTheme.typography.bodyMedium,
  337. )
  338. }
  339. }
  340. @Composable
  341. private fun SortPage(
  342. contentPadding: PaddingValues,
  343. sortingMode: Long,
  344. sortDescending: Boolean,
  345. onItemSelected: (Long) -> Unit,
  346. ) {
  347. Column(
  348. modifier = Modifier
  349. .padding(contentPadding)
  350. .padding(vertical = VerticalPadding)
  351. .verticalScroll(rememberScrollState()),
  352. ) {
  353. val arrowIcon = if (sortDescending) {
  354. Icons.Default.ArrowDownward
  355. } else {
  356. Icons.Default.ArrowUpward
  357. }
  358. SortPageItem(
  359. label = stringResource(R.string.sort_by_source),
  360. statusIcon = arrowIcon.takeIf { sortingMode == Manga.CHAPTER_SORTING_SOURCE },
  361. onClick = { onItemSelected(Manga.CHAPTER_SORTING_SOURCE) },
  362. )
  363. SortPageItem(
  364. label = stringResource(R.string.sort_by_number),
  365. statusIcon = arrowIcon.takeIf { sortingMode == Manga.CHAPTER_SORTING_NUMBER },
  366. onClick = { onItemSelected(Manga.CHAPTER_SORTING_NUMBER) },
  367. )
  368. SortPageItem(
  369. label = stringResource(R.string.sort_by_upload_date),
  370. statusIcon = arrowIcon.takeIf { sortingMode == Manga.CHAPTER_SORTING_UPLOAD_DATE },
  371. onClick = { onItemSelected(Manga.CHAPTER_SORTING_UPLOAD_DATE) },
  372. )
  373. }
  374. }
  375. @Composable
  376. private fun SortPageItem(
  377. label: String,
  378. statusIcon: ImageVector?,
  379. onClick: () -> Unit,
  380. ) {
  381. Row(
  382. modifier = Modifier
  383. .clickable(onClick = onClick)
  384. .fillMaxWidth()
  385. .padding(horizontal = HorizontalPadding, vertical = 12.dp),
  386. verticalAlignment = Alignment.CenterVertically,
  387. horizontalArrangement = Arrangement.spacedBy(24.dp),
  388. ) {
  389. if (statusIcon != null) {
  390. Icon(
  391. imageVector = statusIcon,
  392. contentDescription = null,
  393. tint = MaterialTheme.colorScheme.primary,
  394. )
  395. } else {
  396. Spacer(modifier = Modifier.size(24.dp))
  397. }
  398. Text(
  399. text = label,
  400. style = MaterialTheme.typography.bodyMedium,
  401. )
  402. }
  403. }
  404. @Composable
  405. private fun DisplayPage(
  406. contentPadding: PaddingValues,
  407. displayMode: Long,
  408. onItemSelected: (Long) -> Unit,
  409. ) {
  410. Column(
  411. modifier = Modifier
  412. .padding(contentPadding)
  413. .padding(vertical = VerticalPadding)
  414. .verticalScroll(rememberScrollState()),
  415. ) {
  416. DisplayPageItem(
  417. label = stringResource(R.string.show_title),
  418. selected = displayMode == Manga.CHAPTER_DISPLAY_NAME,
  419. onClick = { onItemSelected(Manga.CHAPTER_DISPLAY_NAME) },
  420. )
  421. DisplayPageItem(
  422. label = stringResource(R.string.show_chapter_number),
  423. selected = displayMode == Manga.CHAPTER_DISPLAY_NUMBER,
  424. onClick = { onItemSelected(Manga.CHAPTER_DISPLAY_NUMBER) },
  425. )
  426. }
  427. }
  428. @Composable
  429. private fun DisplayPageItem(
  430. label: String,
  431. selected: Boolean,
  432. onClick: () -> Unit,
  433. ) {
  434. Row(
  435. modifier = Modifier
  436. .clickable(onClick = onClick)
  437. .fillMaxWidth()
  438. .padding(horizontal = HorizontalPadding, vertical = 12.dp),
  439. verticalAlignment = Alignment.CenterVertically,
  440. horizontalArrangement = Arrangement.spacedBy(24.dp),
  441. ) {
  442. RadioButton(
  443. selected = selected,
  444. onClick = null,
  445. )
  446. Text(
  447. text = label,
  448. style = MaterialTheme.typography.bodyMedium,
  449. )
  450. }
  451. }
  452. private val HorizontalPadding = 24.dp
  453. private val VerticalPadding = 8.dp
  454. @ThemePreviews
  455. @Composable
  456. private fun ChapterSettingsDialogPreview() {
  457. TachiyomiTheme {
  458. Surface {
  459. ChapterSettingsDialogImpl(
  460. onDownloadFilterChanged = {},
  461. onUnreadFilterChanged = {},
  462. onBookmarkedFilterChanged = {},
  463. onSortModeChanged = {},
  464. onDisplayModeChanged = {},
  465. ) {}
  466. }
  467. }
  468. }