ExtensionDetailsScreen.kt 15 KB

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