ExtensionDetailsScreen.kt 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. package eu.kanade.presentation.browse
  2. import android.content.Intent
  3. import android.net.Uri
  4. import android.provider.Settings
  5. import android.util.DisplayMetrics
  6. import androidx.annotation.StringRes
  7. import androidx.compose.foundation.background
  8. import androidx.compose.foundation.clickable
  9. import androidx.compose.foundation.interaction.MutableInteractionSource
  10. import androidx.compose.foundation.layout.Arrangement
  11. import androidx.compose.foundation.layout.Column
  12. import androidx.compose.foundation.layout.PaddingValues
  13. import androidx.compose.foundation.layout.Row
  14. import androidx.compose.foundation.layout.Spacer
  15. import androidx.compose.foundation.layout.fillMaxWidth
  16. import androidx.compose.foundation.layout.height
  17. import androidx.compose.foundation.layout.padding
  18. import androidx.compose.foundation.layout.size
  19. import androidx.compose.foundation.layout.width
  20. import androidx.compose.foundation.lazy.items
  21. import androidx.compose.material.icons.Icons
  22. import androidx.compose.material.icons.outlined.HelpOutline
  23. import androidx.compose.material.icons.outlined.History
  24. import androidx.compose.material.icons.outlined.Settings
  25. import androidx.compose.material3.AlertDialog
  26. import androidx.compose.material3.Button
  27. import androidx.compose.material3.Divider
  28. import androidx.compose.material3.Icon
  29. import androidx.compose.material3.IconButton
  30. import androidx.compose.material3.MaterialTheme
  31. import androidx.compose.material3.OutlinedButton
  32. import androidx.compose.material3.Switch
  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.setValue
  40. import androidx.compose.ui.Alignment
  41. import androidx.compose.ui.Modifier
  42. import androidx.compose.ui.platform.LocalContext
  43. import androidx.compose.ui.platform.LocalUriHandler
  44. import androidx.compose.ui.res.stringResource
  45. import androidx.compose.ui.text.TextStyle
  46. import androidx.compose.ui.text.font.FontWeight
  47. import androidx.compose.ui.text.style.TextAlign
  48. import androidx.compose.ui.unit.dp
  49. import eu.kanade.presentation.browse.components.ExtensionIcon
  50. import eu.kanade.presentation.components.AppBar
  51. import eu.kanade.presentation.components.AppBarActions
  52. import eu.kanade.presentation.components.DIVIDER_ALPHA
  53. import eu.kanade.presentation.components.Divider
  54. import eu.kanade.presentation.components.EmptyScreen
  55. import eu.kanade.presentation.components.LoadingScreen
  56. import eu.kanade.presentation.components.Scaffold
  57. import eu.kanade.presentation.components.ScrollbarLazyColumn
  58. import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
  59. import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
  60. import eu.kanade.presentation.util.horizontalPadding
  61. import eu.kanade.tachiyomi.R
  62. import eu.kanade.tachiyomi.extension.model.Extension
  63. import eu.kanade.tachiyomi.source.ConfigurableSource
  64. import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsPresenter
  65. import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
  66. import eu.kanade.tachiyomi.util.system.LocaleHelper
  67. @Composable
  68. fun ExtensionDetailsScreen(
  69. navigateUp: () -> Unit,
  70. presenter: ExtensionDetailsPresenter,
  71. onClickSourcePreferences: (sourceId: Long) -> Unit,
  72. ) {
  73. val uriHandler = LocalUriHandler.current
  74. Scaffold(
  75. topBar = { scrollBehavior ->
  76. AppBar(
  77. title = stringResource(R.string.label_extension_info),
  78. navigateUp = navigateUp,
  79. actions = {
  80. AppBarActions(
  81. actions = buildList {
  82. if (presenter.extension?.isUnofficial == false) {
  83. add(
  84. AppBar.Action(
  85. title = stringResource(R.string.whats_new),
  86. icon = Icons.Outlined.History,
  87. onClick = { uriHandler.openUri(presenter.getChangelogUrl()) },
  88. ),
  89. )
  90. add(
  91. AppBar.Action(
  92. title = stringResource(R.string.action_faq_and_guides),
  93. icon = Icons.Outlined.HelpOutline,
  94. onClick = { uriHandler.openUri(presenter.getReadmeUrl()) },
  95. ),
  96. )
  97. }
  98. addAll(
  99. listOf(
  100. AppBar.OverflowAction(
  101. title = stringResource(R.string.action_enable_all),
  102. onClick = { presenter.toggleSources(true) },
  103. ),
  104. AppBar.OverflowAction(
  105. title = stringResource(R.string.action_disable_all),
  106. onClick = { presenter.toggleSources(false) },
  107. ),
  108. AppBar.OverflowAction(
  109. title = stringResource(R.string.pref_clear_cookies),
  110. onClick = { presenter.clearCookies() },
  111. ),
  112. ),
  113. )
  114. },
  115. )
  116. },
  117. scrollBehavior = scrollBehavior,
  118. )
  119. },
  120. ) { paddingValues ->
  121. ExtensionDetails(paddingValues, presenter, onClickSourcePreferences)
  122. }
  123. }
  124. @Composable
  125. private fun ExtensionDetails(
  126. contentPadding: PaddingValues,
  127. presenter: ExtensionDetailsPresenter,
  128. onClickSourcePreferences: (sourceId: Long) -> Unit,
  129. ) {
  130. when {
  131. presenter.isLoading -> LoadingScreen()
  132. presenter.extension == null -> EmptyScreen(
  133. textResource = R.string.empty_screen,
  134. modifier = Modifier.padding(contentPadding),
  135. )
  136. else -> {
  137. val context = LocalContext.current
  138. val extension = presenter.extension
  139. var showNsfwWarning by remember { mutableStateOf(false) }
  140. ScrollbarLazyColumn(
  141. contentPadding = contentPadding,
  142. ) {
  143. when {
  144. extension.isUnofficial ->
  145. item {
  146. WarningBanner(R.string.unofficial_extension_message)
  147. }
  148. extension.isObsolete ->
  149. item {
  150. WarningBanner(R.string.obsolete_extension_message)
  151. }
  152. }
  153. item {
  154. DetailsHeader(
  155. extension = extension,
  156. onClickUninstall = { presenter.uninstallExtension() },
  157. onClickAppInfo = {
  158. Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
  159. data = Uri.fromParts("package", extension.pkgName, null)
  160. context.startActivity(this)
  161. }
  162. },
  163. onClickAgeRating = {
  164. showNsfwWarning = true
  165. },
  166. )
  167. }
  168. items(
  169. items = presenter.sources,
  170. key = { it.source.id },
  171. ) { source ->
  172. SourceSwitchPreference(
  173. modifier = Modifier.animateItemPlacement(),
  174. source = source,
  175. onClickSourcePreferences = onClickSourcePreferences,
  176. onClickSource = { presenter.toggleSource(it) },
  177. )
  178. }
  179. }
  180. if (showNsfwWarning) {
  181. NsfwWarningDialog(
  182. onClickConfirm = {
  183. showNsfwWarning = false
  184. },
  185. )
  186. }
  187. }
  188. }
  189. }
  190. @Composable
  191. private fun WarningBanner(@StringRes textRes: Int) {
  192. Text(
  193. text = stringResource(textRes),
  194. modifier = Modifier
  195. .fillMaxWidth()
  196. .background(MaterialTheme.colorScheme.error)
  197. .padding(16.dp),
  198. color = MaterialTheme.colorScheme.onError,
  199. style = MaterialTheme.typography.bodyMedium,
  200. textAlign = TextAlign.Center,
  201. )
  202. }
  203. @Composable
  204. private fun DetailsHeader(
  205. extension: Extension,
  206. onClickAgeRating: () -> Unit,
  207. onClickUninstall: () -> Unit,
  208. onClickAppInfo: () -> Unit,
  209. ) {
  210. val context = LocalContext.current
  211. Column {
  212. Column(
  213. modifier = Modifier
  214. .fillMaxWidth()
  215. .padding(
  216. start = horizontalPadding,
  217. end = horizontalPadding,
  218. top = 16.dp,
  219. bottom = 8.dp,
  220. ),
  221. horizontalAlignment = Alignment.CenterHorizontally,
  222. ) {
  223. ExtensionIcon(
  224. modifier = Modifier
  225. .size(112.dp),
  226. extension = extension,
  227. density = DisplayMetrics.DENSITY_XXXHIGH,
  228. )
  229. Text(
  230. text = extension.name,
  231. style = MaterialTheme.typography.headlineSmall,
  232. )
  233. val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
  234. Text(
  235. text = strippedPkgName,
  236. style = MaterialTheme.typography.bodySmall,
  237. )
  238. }
  239. Row(
  240. modifier = Modifier
  241. .fillMaxWidth()
  242. .padding(
  243. horizontal = horizontalPadding * 2,
  244. vertical = 8.dp,
  245. ),
  246. horizontalArrangement = Arrangement.SpaceEvenly,
  247. verticalAlignment = Alignment.CenterVertically,
  248. ) {
  249. InfoText(
  250. modifier = Modifier.weight(1f),
  251. primaryText = extension.versionName,
  252. secondaryText = stringResource(R.string.ext_info_version),
  253. )
  254. InfoDivider()
  255. InfoText(
  256. modifier = Modifier.weight(if (extension.isNsfw) 1.5f else 1f),
  257. primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context),
  258. secondaryText = stringResource(R.string.ext_info_language),
  259. )
  260. if (extension.isNsfw) {
  261. InfoDivider()
  262. InfoText(
  263. modifier = Modifier.weight(1f),
  264. primaryText = stringResource(R.string.ext_nsfw_short),
  265. primaryTextStyle = MaterialTheme.typography.bodyLarge.copy(
  266. color = MaterialTheme.colorScheme.error,
  267. fontWeight = FontWeight.Medium,
  268. ),
  269. secondaryText = stringResource(R.string.ext_info_age_rating),
  270. onClick = onClickAgeRating,
  271. )
  272. }
  273. }
  274. Row(
  275. modifier = Modifier.padding(
  276. start = horizontalPadding,
  277. end = horizontalPadding,
  278. top = 8.dp,
  279. bottom = 16.dp,
  280. ),
  281. ) {
  282. OutlinedButton(
  283. modifier = Modifier.weight(1f),
  284. onClick = onClickUninstall,
  285. ) {
  286. Text(stringResource(R.string.ext_uninstall))
  287. }
  288. Spacer(Modifier.width(16.dp))
  289. Button(
  290. modifier = Modifier.weight(1f),
  291. onClick = onClickAppInfo,
  292. ) {
  293. Text(
  294. text = stringResource(R.string.ext_app_info),
  295. color = MaterialTheme.colorScheme.onPrimary,
  296. )
  297. }
  298. }
  299. Divider()
  300. }
  301. }
  302. @Composable
  303. private fun InfoText(
  304. modifier: Modifier,
  305. primaryText: String,
  306. primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
  307. secondaryText: String,
  308. onClick: (() -> Unit)? = null,
  309. ) {
  310. val interactionSource = remember { MutableInteractionSource() }
  311. val clickableModifier = if (onClick != null) {
  312. Modifier.clickable(interactionSource, indication = null) { onClick() }
  313. } else {
  314. Modifier
  315. }
  316. Column(
  317. modifier = modifier.then(clickableModifier),
  318. horizontalAlignment = Alignment.CenterHorizontally,
  319. verticalArrangement = Arrangement.Center,
  320. ) {
  321. Text(
  322. text = primaryText,
  323. textAlign = TextAlign.Center,
  324. style = primaryTextStyle,
  325. )
  326. Text(
  327. text = secondaryText + if (onClick != null) " ⓘ" else "",
  328. textAlign = TextAlign.Center,
  329. style = MaterialTheme.typography.bodyMedium,
  330. color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
  331. )
  332. }
  333. }
  334. @Composable
  335. private fun InfoDivider() {
  336. Divider(
  337. modifier = Modifier
  338. .height(20.dp)
  339. .width(1.dp),
  340. color = MaterialTheme.colorScheme.onSurface.copy(alpha = DIVIDER_ALPHA),
  341. )
  342. }
  343. @Composable
  344. private fun SourceSwitchPreference(
  345. modifier: Modifier = Modifier,
  346. source: ExtensionSourceItem,
  347. onClickSourcePreferences: (sourceId: Long) -> Unit,
  348. onClickSource: (sourceId: Long) -> Unit,
  349. ) {
  350. val context = LocalContext.current
  351. TextPreferenceWidget(
  352. modifier = modifier,
  353. title = if (source.labelAsName) {
  354. source.source.toString()
  355. } else {
  356. LocaleHelper.getSourceDisplayName(source.source.lang, context)
  357. },
  358. widget = {
  359. Row(
  360. verticalAlignment = Alignment.CenterVertically,
  361. ) {
  362. if (source.source is ConfigurableSource) {
  363. IconButton(onClick = { onClickSourcePreferences(source.source.id) }) {
  364. Icon(
  365. imageVector = Icons.Outlined.Settings,
  366. contentDescription = stringResource(R.string.label_settings),
  367. tint = MaterialTheme.colorScheme.onSurface,
  368. )
  369. }
  370. }
  371. Switch(
  372. checked = source.enabled,
  373. onCheckedChange = null,
  374. modifier = Modifier.padding(start = TrailingWidgetBuffer),
  375. )
  376. }
  377. },
  378. onPreferenceClick = { onClickSource(source.source.id) },
  379. )
  380. }
  381. @Composable
  382. private fun NsfwWarningDialog(
  383. onClickConfirm: () -> Unit,
  384. ) {
  385. AlertDialog(
  386. text = {
  387. Text(text = stringResource(R.string.ext_nsfw_warning))
  388. },
  389. confirmButton = {
  390. TextButton(onClick = onClickConfirm) {
  391. Text(text = stringResource(android.R.string.ok))
  392. }
  393. },
  394. onDismissRequest = onClickConfirm,
  395. )
  396. }