ExtensionsScreen.kt 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. package eu.kanade.presentation.browse
  2. import androidx.annotation.StringRes
  3. import androidx.compose.foundation.combinedClickable
  4. import androidx.compose.foundation.layout.Arrangement
  5. import androidx.compose.foundation.layout.Column
  6. import androidx.compose.foundation.layout.Row
  7. import androidx.compose.foundation.layout.RowScope
  8. import androidx.compose.foundation.layout.WindowInsets
  9. import androidx.compose.foundation.layout.asPaddingValues
  10. import androidx.compose.foundation.layout.navigationBars
  11. import androidx.compose.foundation.layout.padding
  12. import androidx.compose.foundation.lazy.items
  13. import androidx.compose.material.icons.Icons
  14. import androidx.compose.material.icons.filled.Close
  15. import androidx.compose.material3.AlertDialog
  16. import androidx.compose.material3.Button
  17. import androidx.compose.material3.Icon
  18. import androidx.compose.material3.IconButton
  19. import androidx.compose.material3.LocalTextStyle
  20. import androidx.compose.material3.MaterialTheme
  21. import androidx.compose.material3.Text
  22. import androidx.compose.material3.TextButton
  23. import androidx.compose.runtime.Composable
  24. import androidx.compose.runtime.getValue
  25. import androidx.compose.runtime.mutableStateOf
  26. import androidx.compose.runtime.remember
  27. import androidx.compose.runtime.setValue
  28. import androidx.compose.ui.Alignment
  29. import androidx.compose.ui.Modifier
  30. import androidx.compose.ui.platform.LocalContext
  31. import androidx.compose.ui.res.stringResource
  32. import androidx.compose.ui.text.style.TextOverflow
  33. import androidx.compose.ui.unit.dp
  34. import com.google.accompanist.swiperefresh.SwipeRefresh
  35. import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
  36. import eu.kanade.presentation.browse.components.BaseBrowseItem
  37. import eu.kanade.presentation.browse.components.ExtensionIcon
  38. import eu.kanade.presentation.components.EmptyScreen
  39. import eu.kanade.presentation.components.FastScrollLazyColumn
  40. import eu.kanade.presentation.components.LoadingScreen
  41. import eu.kanade.presentation.components.SwipeRefreshIndicator
  42. import eu.kanade.presentation.theme.header
  43. import eu.kanade.presentation.util.bottomNavPaddingValues
  44. import eu.kanade.presentation.util.horizontalPadding
  45. import eu.kanade.presentation.util.plus
  46. import eu.kanade.presentation.util.topPaddingValues
  47. import eu.kanade.tachiyomi.R
  48. import eu.kanade.tachiyomi.extension.model.Extension
  49. import eu.kanade.tachiyomi.extension.model.InstallStep
  50. import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
  51. import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter
  52. import eu.kanade.tachiyomi.util.system.LocaleHelper
  53. @Composable
  54. fun ExtensionScreen(
  55. presenter: ExtensionsPresenter,
  56. onLongClickItem: (Extension) -> Unit,
  57. onClickItemCancel: (Extension) -> Unit,
  58. onInstallExtension: (Extension.Available) -> Unit,
  59. onUninstallExtension: (Extension) -> Unit,
  60. onUpdateExtension: (Extension.Installed) -> Unit,
  61. onTrustExtension: (Extension.Untrusted) -> Unit,
  62. onOpenExtension: (Extension.Installed) -> Unit,
  63. onClickUpdateAll: () -> Unit,
  64. onRefresh: () -> Unit,
  65. ) {
  66. SwipeRefresh(
  67. state = rememberSwipeRefreshState(presenter.isRefreshing),
  68. indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) },
  69. onRefresh = onRefresh,
  70. ) {
  71. when {
  72. presenter.isLoading -> LoadingScreen()
  73. presenter.isEmpty -> EmptyScreen(R.string.empty_screen)
  74. else -> {
  75. ExtensionContent(
  76. state = presenter,
  77. onLongClickItem = onLongClickItem,
  78. onClickItemCancel = onClickItemCancel,
  79. onInstallExtension = onInstallExtension,
  80. onUninstallExtension = onUninstallExtension,
  81. onUpdateExtension = onUpdateExtension,
  82. onTrustExtension = onTrustExtension,
  83. onOpenExtension = onOpenExtension,
  84. onClickUpdateAll = onClickUpdateAll,
  85. )
  86. }
  87. }
  88. }
  89. }
  90. @Composable
  91. fun ExtensionContent(
  92. state: ExtensionsState,
  93. onLongClickItem: (Extension) -> Unit,
  94. onClickItemCancel: (Extension) -> Unit,
  95. onInstallExtension: (Extension.Available) -> Unit,
  96. onUninstallExtension: (Extension) -> Unit,
  97. onUpdateExtension: (Extension.Installed) -> Unit,
  98. onTrustExtension: (Extension.Untrusted) -> Unit,
  99. onOpenExtension: (Extension.Installed) -> Unit,
  100. onClickUpdateAll: () -> Unit,
  101. ) {
  102. var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
  103. FastScrollLazyColumn(
  104. contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
  105. ) {
  106. items(
  107. items = state.items,
  108. key = {
  109. when (it) {
  110. is ExtensionUiModel.Header.Resource -> it.textRes
  111. is ExtensionUiModel.Header.Text -> it.text
  112. is ExtensionUiModel.Item -> it.key()
  113. }
  114. },
  115. contentType = {
  116. when (it) {
  117. is ExtensionUiModel.Item -> "item"
  118. else -> "header"
  119. }
  120. },
  121. ) { item ->
  122. when (item) {
  123. is ExtensionUiModel.Header.Resource -> {
  124. val action: @Composable RowScope.() -> Unit =
  125. if (item.textRes == R.string.ext_updates_pending) {
  126. {
  127. Button(onClick = { onClickUpdateAll() }) {
  128. Text(
  129. text = stringResource(R.string.ext_update_all),
  130. style = LocalTextStyle.current.copy(
  131. color = MaterialTheme.colorScheme.onPrimary,
  132. ),
  133. )
  134. }
  135. }
  136. } else {
  137. {}
  138. }
  139. ExtensionHeader(
  140. textRes = item.textRes,
  141. modifier = Modifier.animateItemPlacement(),
  142. action = action,
  143. )
  144. }
  145. is ExtensionUiModel.Header.Text -> {
  146. ExtensionHeader(
  147. text = item.text,
  148. modifier = Modifier.animateItemPlacement(),
  149. )
  150. }
  151. is ExtensionUiModel.Item -> {
  152. ExtensionItem(
  153. modifier = Modifier.animateItemPlacement(),
  154. item = item,
  155. onClickItem = {
  156. when (it) {
  157. is Extension.Available -> onInstallExtension(it)
  158. is Extension.Installed -> onOpenExtension(it)
  159. is Extension.Untrusted -> { trustState = it }
  160. }
  161. },
  162. onLongClickItem = onLongClickItem,
  163. onClickItemCancel = onClickItemCancel,
  164. onClickItemAction = {
  165. when (it) {
  166. is Extension.Available -> onInstallExtension(it)
  167. is Extension.Installed -> {
  168. if (it.hasUpdate) {
  169. onUpdateExtension(it)
  170. } else {
  171. onOpenExtension(it)
  172. }
  173. }
  174. is Extension.Untrusted -> { trustState = it }
  175. }
  176. },
  177. )
  178. }
  179. }
  180. }
  181. }
  182. if (trustState != null) {
  183. ExtensionTrustDialog(
  184. onClickConfirm = {
  185. onTrustExtension(trustState!!)
  186. trustState = null
  187. },
  188. onClickDismiss = {
  189. onUninstallExtension(trustState!!)
  190. trustState = null
  191. },
  192. onDismissRequest = {
  193. trustState = null
  194. },
  195. )
  196. }
  197. }
  198. @Composable
  199. fun ExtensionItem(
  200. modifier: Modifier = Modifier,
  201. item: ExtensionUiModel.Item,
  202. onClickItem: (Extension) -> Unit,
  203. onLongClickItem: (Extension) -> Unit,
  204. onClickItemCancel: (Extension) -> Unit,
  205. onClickItemAction: (Extension) -> Unit,
  206. ) {
  207. val (extension, installStep) = item
  208. BaseBrowseItem(
  209. modifier = modifier
  210. .combinedClickable(
  211. onClick = { onClickItem(extension) },
  212. onLongClick = { onLongClickItem(extension) },
  213. ),
  214. onClickItem = { onClickItem(extension) },
  215. onLongClickItem = { onLongClickItem(extension) },
  216. icon = {
  217. ExtensionIcon(extension = extension)
  218. },
  219. action = {
  220. ExtensionItemActions(
  221. extension = extension,
  222. installStep = installStep,
  223. onClickItemCancel = onClickItemCancel,
  224. onClickItemAction = onClickItemAction,
  225. )
  226. },
  227. ) {
  228. ExtensionItemContent(
  229. extension = extension,
  230. modifier = Modifier.weight(1f),
  231. )
  232. }
  233. }
  234. @Composable
  235. fun ExtensionItemContent(
  236. extension: Extension,
  237. modifier: Modifier = Modifier,
  238. ) {
  239. val context = LocalContext.current
  240. val warning = remember(extension) {
  241. when {
  242. extension is Extension.Untrusted -> R.string.ext_untrusted
  243. extension is Extension.Installed && extension.isUnofficial -> R.string.ext_unofficial
  244. extension is Extension.Installed && extension.isObsolete -> R.string.ext_obsolete
  245. extension.isNsfw -> R.string.ext_nsfw_short
  246. else -> null
  247. }
  248. }
  249. Column(
  250. modifier = modifier.padding(start = horizontalPadding),
  251. ) {
  252. Text(
  253. text = extension.name,
  254. maxLines = 1,
  255. overflow = TextOverflow.Ellipsis,
  256. style = MaterialTheme.typography.bodyMedium,
  257. )
  258. Row(
  259. horizontalArrangement = Arrangement.spacedBy(4.dp),
  260. ) {
  261. if (extension.lang.isNullOrEmpty().not()) {
  262. Text(
  263. text = LocaleHelper.getSourceDisplayName(extension.lang, context),
  264. style = MaterialTheme.typography.bodySmall,
  265. )
  266. }
  267. if (extension.versionName.isNotEmpty()) {
  268. Text(
  269. text = extension.versionName,
  270. style = MaterialTheme.typography.bodySmall,
  271. )
  272. }
  273. if (warning != null) {
  274. Text(
  275. text = stringResource(warning).uppercase(),
  276. maxLines = 1,
  277. overflow = TextOverflow.Ellipsis,
  278. style = MaterialTheme.typography.bodySmall.copy(
  279. color = MaterialTheme.colorScheme.error,
  280. ),
  281. )
  282. }
  283. }
  284. }
  285. }
  286. @Composable
  287. fun ExtensionItemActions(
  288. extension: Extension,
  289. installStep: InstallStep,
  290. modifier: Modifier = Modifier,
  291. onClickItemCancel: (Extension) -> Unit = {},
  292. onClickItemAction: (Extension) -> Unit = {},
  293. ) {
  294. val isIdle = remember(installStep) {
  295. installStep == InstallStep.Idle || installStep == InstallStep.Error
  296. }
  297. Row(modifier = modifier) {
  298. TextButton(
  299. onClick = { onClickItemAction(extension) },
  300. enabled = isIdle,
  301. ) {
  302. Text(
  303. text = when (installStep) {
  304. InstallStep.Pending -> stringResource(R.string.ext_pending)
  305. InstallStep.Downloading -> stringResource(R.string.ext_downloading)
  306. InstallStep.Installing -> stringResource(R.string.ext_installing)
  307. InstallStep.Installed -> stringResource(R.string.ext_installed)
  308. InstallStep.Error -> stringResource(R.string.action_retry)
  309. InstallStep.Idle -> {
  310. when (extension) {
  311. is Extension.Installed -> {
  312. if (extension.hasUpdate) {
  313. stringResource(R.string.ext_update)
  314. } else {
  315. stringResource(R.string.action_settings)
  316. }
  317. }
  318. is Extension.Untrusted -> stringResource(R.string.ext_trust)
  319. is Extension.Available -> stringResource(R.string.ext_install)
  320. }
  321. }
  322. },
  323. style = LocalTextStyle.current.copy(
  324. color = if (isIdle) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceTint,
  325. ),
  326. )
  327. }
  328. if (isIdle.not()) {
  329. IconButton(onClick = { onClickItemCancel(extension) }) {
  330. Icon(
  331. imageVector = Icons.Default.Close,
  332. contentDescription = "",
  333. tint = MaterialTheme.colorScheme.onBackground,
  334. )
  335. }
  336. }
  337. }
  338. }
  339. @Composable
  340. fun ExtensionHeader(
  341. @StringRes textRes: Int,
  342. modifier: Modifier = Modifier,
  343. action: @Composable RowScope.() -> Unit = {},
  344. ) {
  345. ExtensionHeader(
  346. text = stringResource(textRes),
  347. modifier = modifier,
  348. action = action,
  349. )
  350. }
  351. @Composable
  352. fun ExtensionHeader(
  353. text: String,
  354. modifier: Modifier = Modifier,
  355. action: @Composable RowScope.() -> Unit = {},
  356. ) {
  357. Row(
  358. modifier = modifier.padding(horizontal = horizontalPadding),
  359. verticalAlignment = Alignment.CenterVertically,
  360. ) {
  361. Text(
  362. text = text,
  363. modifier = Modifier
  364. .padding(vertical = 8.dp)
  365. .weight(1f),
  366. style = MaterialTheme.typography.header,
  367. )
  368. action()
  369. }
  370. }
  371. @Composable
  372. fun ExtensionTrustDialog(
  373. onClickConfirm: () -> Unit,
  374. onClickDismiss: () -> Unit,
  375. onDismissRequest: () -> Unit,
  376. ) {
  377. AlertDialog(
  378. title = {
  379. Text(text = stringResource(R.string.untrusted_extension))
  380. },
  381. text = {
  382. Text(text = stringResource(R.string.untrusted_extension_message))
  383. },
  384. confirmButton = {
  385. TextButton(onClick = onClickConfirm) {
  386. Text(text = stringResource(R.string.ext_trust))
  387. }
  388. },
  389. dismissButton = {
  390. TextButton(onClick = onClickDismiss) {
  391. Text(text = stringResource(R.string.ext_uninstall))
  392. }
  393. },
  394. onDismissRequest = onDismissRequest,
  395. )
  396. }