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