ExtensionDetailsScreen.kt 15 KB

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